这是一个前端面试常常考的根底考点,很多初学者在这个问题上都容易踩坑,包含我也是经常性蒙圈。所以这次决定将他们梳理下来,加深本人的了解。如果有出错的中央,欢送斧正。
this 是什么
this 关键字是 Javascript ES5 中最简单的机制之一。ES6 中新增的箭头函数,很大水平上防止了应用 this 所产生的谬误。然而在 ES5 中,有时候咱们会谬误的判断了 this 的指向。其实对于 this 的指向,始终保持一个原理:this 永远指向最初调用它的那个对象,记住了这句话,this 的指向你曾经理解一半了。
想要理解 this 的指向,咱们首先要理解 this 的四种绑定形式:隐式绑定、显示绑定、window 绑定、new 绑定。
this 的四种绑定形式
隐式绑定
执行绑定的第一个也是最常见的规定为 隐式绑定,它 80% 的状况下会通知你 this 指向的对象是什么。
咱们先来看一个简略的例子:
const user = { name: 'Cherry', age: 27, getName() { console.log(`Hello, my name is ${this.name}`) } } user.getName() // Hello, my name is Cherry
当咱们执行 user.getName()
时,会打印出Hello, my name is Cherry
。
如果你要调用 user 对象上的 getName 办法,你会用到点.
这就是所谓隐式绑定,函数被调用时先看一看点号左侧。如果有“点”就查看“点”左侧的对象,这个对象就是 this 的援用。
在下面的例子中,user 在“点号左侧”意味着 this 援用了 user 对象。所以就如同 在 getName 办法的外部 JavaScript 解释器把 this 变成了 user。
所以,你能够得出这样的论断:应用对象来调用其外部的一个办法,该办法的 this 是指向对象自身的。这就是所谓隐式绑定,你也能够这样认为:JavaScript解释器在执行 user.getName()
时,将其转化为了:
user.getName.call(user);
咱们将代码减少一层调用:
const user = { name: 'Cherry', age: 27, getName() { console.log(`Hello, my name is ${this.name}`) }, mother: { name: 'Susan', getName() { console.log(`Hello, my name is ${this.name}`) } }}user.getName() // Hello, my name is Cherryuser.mother.getName() // Hello, my name is Susan
正如方才所说:this 永远指向最初调用它的那个对象,那么“点”左侧的对象即为后调用该办法的对象,this 指向该对象。然而,如果没有点呢?这就为咱们引出了下一条规定:
显示绑定
对于显示绑定,咱们能够通过 call 来设置函数执行上下文的 this 指向,比方上面这段代码:
function getName () { console.log(`Hello, my name is ${this.myName}`)}let user = { myName: 'Cherry', age: 27,}getName.call(user) // Hello, my name is Cherry
执行这段代码,而后察看输入后果,你会发现 getName 函数外部的 this 曾经指向了 user 对象。
其实除了 call 办法,咱们还能够应用 bind 和 apply 办法来设置函数执行上下文中的 this,它们在应用上有一些区别,文章的第六大节会对 call、apply、bind 进行具体的介绍,这里我就不过多赘述了。
window 绑定
咱们在方才的例子的根底上批改一下:
function getName () { console.log(`Hello, my name is ${this.myName}`)}let user = { myName: 'Cherry', age: 27,}getName();
置信大家都晓得为什么打印进去的是 My name is undefined,因为正如后面所说的,如果你想用 user 做上下文调用 getName,你能够应用 .call、.apply 或 .bind。但如果咱们没有用这些办法,而是间接和平时一样间接调用,JavaScript 会默认 this 指向 window 对象。然而 window 对象中并没有 myName 属性,所以会打印 “My name is undefined“。
在 ES5 增加的 严格模式 中,JavaScript 不会默认 this 指向 window 对象,而会正确地把 this 放弃为 undefined。
例如:
'use strict'age = 27function sayAge () { console.log(`Hello, my age is ${this.age}`)}sayAge() // TypeError: Cannot read property 'age' of undefined
new 绑定
第四条判断 this 援用的规定是 new 绑定。每当用 new 调用函数时,JavaScript 解释器都会在底层创立一个全新的对象并把这个对象当做 this。
这看起来就像创立了新的函数,但实际上 JavaScript 函数是从新创立的对象。
例如:
function User (name, age) { /* JavaScript 会在底层创立一个新对象 `this`,它会代理不在 User 原型链上的属性。 如果一个函数用 new 关键字调用,this 就会指向解释器创立的新对象。 */ this.name = name this.age = age}const me = new User('Cherry', 27)
伪代码示意:
var me = new User("Cherry","27");new User{ var object = {}; object.__proto__ = User.prototype; var result = User.call(object,"Cherry","27"); return typeof result === 'object'? result : object;}
new 的过程:
- 创立一个空对象 object;
- 将新创建的空对象的隐式原型指向其构造函数的显示原型;
- 应用 call 扭转 this 的指向;
- 如果无返回值或者返回一个非对象值,则将 object 返回作为新对象;如果返回值是一个新对象的话那么间接间接返回该对象。
所以咱们能够看到,在 new 的过程中,其实是应用 call 扭转了 this 的指向。
this 的指向
后面讲了对于 this 的四种绑定形式,咱们对于 this 的指向应该也有了一些本人的了解,还记得咱们之前说的吗?this 永远指向最初调用它的那个对象,咱们记好这句话来练习上面的例子:
练1:
var name = "window";function fn() { var name = "Cherry"; console.log(this.name); // window console.log("inner:" + this); // inner: Window}fn();console.log("outer:" + this) // outer: Window
咱们看最初调用 fn 的中央 fn();
,后面没有“点”,Javascript 调用的对象默认指向了全局对象 window,这就相当于是 window.fn();
所以依据刚刚的那句话“this 永远指向最初调用它的那个对象”,this 指向的就是 window。绑定规定是Window绑定。
留神,这里咱们没有应用严格模式,如果应用严格模式的话,全局对象就是 undefined,那么就会报错 Uncaught TypeError: Cannot read property 'name' of undefined。
练2:
var name = "window";var user = { name: "Cherry", fn: function () { console.log(this.name); // Cherry }}user.fn();
依据上文所说,咱们看到函数 fn 左侧有“点”,“点”的左侧是 user,所以 fn 是对象 user 调用的。所以打印的值就是 user 中的 name 的值。绑定规定是隐式绑定。
练3:
var name = "window";function fnA(){ var name = "Cherry"; function fnB(){ console.log(this.name); // window } //在A函数外部调用B函数 fnB();}//调用A函数fnA();
嵌套函数中的 this 不会从外层函数中继承。在函数执行环境中应用 this 时,如果函数没有显著的作为非 window 对象的属性,而只是定义了函数,这个函数中的 this 依然默认指向 window 对象。
练4:
var name = "window";var user = { name: "Cherry", fn: function () { console.log(this.name); // Cherry }}window.user.fn();
这里打印 Cherry 的起因也是因为刚刚那句话“this 永远指向最初调用它的那个对象”,最初调用它的对象依然是对象 user。
咱们改变一下:
var name = "window";var user = { // name: "Cherry", fn: function () { console.log(this.name); // undefined }}window.user.fn();
这是因为调用 fn 的是 user 对象,也就是说 fn 的外部的 this 是对象 user,而对象 user 中并没有对 name 进行定义,所以 log 的 this.name 的值是 undefined。
这个例子还是阐明了:this 永远指向最初调用它的那个对象,因为最初调用 fn 的对象是 user,所以就算 user 中没有 name 这个属性,也不会持续向上一个对象寻找 this.name,而是间接输入 undefined。
练5:(这个例子稍稍有点坑)
var name = "window";var user = { name : null, // name: "Cherry", fn : function () { console.log(this.name); // window }}var f = user.fn;f();
这里你可能会有疑难,为什么不是 Cherry?因为这里尽管将 user 对象的 fn
办法赋值给变量 f
了,然而没有调用,再接着跟我念这一句话:“this 永远指向最初调用它的那个对象”,因为刚刚的 f
并没有调用,所以 fn()
最初依然是被 window 调用的。所以 this 指向的也就是 window。
由以上五个练习咱们能够看出,this 的指向并不是在创立的时候就能够确定的,在 es5 中,this永远指向最初调用它的那个对象。
如何扭转 this 的指向
扭转 this 的指向我总结有以下几种办法:
- 应用 ES6 的箭头函数
- 在函数外部应用 _this = this
- 应用 apply、call、bind
- new 实例化一个对象
咱们看上面的例子:
var name = "window";var user = { name : "Cherry", fn1: function() { console.log(this.name) }, fn2: function() { setTimeout(function () { this.fn1() },100); }};user.fn2() // this.fn1 is not a function
咱们逐个细说一下这个例子:fn2()
是被 user调用的,所以fn2
中的 this 应该指向 user。然而fn2
中又调用了 window 中的 setTimeout 办法。所以在 setTimeout 办法中的 this 指向的是后调用它的对象 window。然而在 window 中并没有 fn1 函数。所以抛出谬误:this.fn1 is not a function。
如果咱们想正确的调用 user 中的 fn1()
,应该怎么做呢?咱们把这个例子作为 demo 进行革新。
箭头函数
家喻户晓,ES6 的箭头函数是能够防止 ES5 中应用 this 的坑的。“所有的箭头函数都没有本人的this,都指向外层。”--这句话就是箭头函数的精华。箭头函数的this,总是指向定义时所在的对象,而不是运行时所在的对象。这句话说的太含糊了,最好改成:总是指向所在函数运行时的this。
下面例子咱们应用箭头函数扭转this的指向如下:
var name = "window";var user = { name : "Cherry", fn1: function () { console.log(this.name) }, fn2: function () { setTimeout( () => { this.fn1() },100); }};user.fn2() // Cherry
对于箭头函数,咱们还须要留神以下几点:
- 函数体内的this就是定义时所在的对象,而非调用时所在的对象,和一般函数相同。
- 箭头函数无奈用做构造函数,即不能应用new调用
- 不能应用arguments对象,函数中不存在这个对象。
- 不可应用yield命令,即无奈用做Generator函数。
其中第一点尤其值得注意,之所以this是固定的,是因为箭头函数自身没有this,箭头函数的this不是本人的。所以不能批改,也正因为没有this,所以不能用作构造函数。这些限度都是因为没有this导致的。
在函数外部应用 _this = this
如果不应用 ES6,那么这种形式应该是最简略的不会出错的形式了,咱们是先将调用这个函数的对象保留在变量 _this 中,而后在函数中都应用这个 _this,这样 _this 就不会扭转了。
var name = "window";var user = { name : "Cherry", fn1: function () { console.log(this.name) }, fn2: function () { var _this = this; setTimeout( function() { _this.fn1() },100); }};user.fn2() // Cherry
这个例子中,在 fn2 中,首先设置 var _this = this;,这里的 this 是调用 fn2
的对象 user,为了避免在 fn2
中的 setTimeout 被 window 调用而导致的在 setTimeout 中的 this 为 window。咱们将 this(指向变量 user) 赋值给一个变量 _this,这样,在 fn2
中咱们应用 _this 就是指向对象 user 了。
应用 apply、call、bind
应用 apply、call、bind 函数也是能够扭转 this 的指向的,成为显示绑定,咱们先来看一下是怎么实现的:
应用 apply()
var user = { name: "Cherry", fn1: function() { console.log(this.name) }, fn2: function() { setTimeout(function () { this.fn1() }.apply(user), 100); }};user.fn2() // Cherry
应用 call()
var user = { name: "Cherry", fn1: function() { console.log(this.name) }, fn2: function() { setTimeout(function () { this.fn1() }.call(user), 100); }};user.fn2() // Cherry
应用 bind()
var user = { name: "Cherry", fn1: function() { console.log(this.name) }, fn2: function() { setTimeout(function () { this.fn1() }.bind(user)(), 100); }};user.fn2() // Cherry
apply、call、bind 的区别
刚刚咱们曾经介绍了 apply、call、bind 都是能够扭转 this 的指向的,然而这三个函数稍有不同。
在 MDN 中定义 apply 如下;
apply() 办法调用一个函数, 其具备一个指定的this值,以及作为一个数组(或相似数组的对象)提供的参数
apply 和 call 的区别
其实 apply 和 call 根本相似,他们的区别只是传入的参数不同。
call 的语法为:
fun.call(thisArg[, arg1[, arg2[, ...]]])
所以 apply 和 call 的区别是 call 办法承受的是若干个参数列表,而 apply 接管的是一个蕴含多个参数的数组。
apply()的应用办法:
var user ={ name: "Cherry", fn: function(a,b) { console.log(a + b) }}var newUser = user.fn;newUser.apply(user,[1,2]) // 3
call()的应用办法:
var user ={ name: "Cherry", fn: function(a,b) { console.log(a + b) }}var newUser =user.fn;newUser.call(user, 1, 2) // 3
但凡事都有例外:
若将null、undefined等值作为call、apply的第一个参数,那么理论调用时会被疏忽,从而利用到Window绑定规定,即绑定到window上,有些时候咱们不关怀上下文,只关怀参数时,能够这样做。
但这样其实存在这一些潜在的危险,绑定到window很可能无心中增加或批改了全局变量,造成一些荫蔽的bug。所以为了避免这种状况呈现,能够将第一个参数绑定为一个空对象。当然具体还是看需要,这只是倡议。
bind 和 apply、call 区别
咱们先应用 bind 试一下刚刚的例子:
var user ={ name: "Cherry", fn: function(a,b) { console.log(a + b) }}var newUser = user.fn;nreUser.bind(user,1,2)
咱们会发现并没有输入,这是为什么呢,咱们来看一下 MDN 上的文档阐明:
bind()办法创立一个新的函数, 当被调用时,将其this关键字设置为提供的值,在调用新函数时,在任何提供之前提供一个给定的参数序列。
所以咱们能够看出,bind 是创立一个新的函数,咱们必须要手动去调用:
var user ={ name: "Cherry", fn: function (a,b) { console.log( a + b) }}var newUser = user.fn;newUser.bind(user,1,2)() // 3
以上就是三种显示绑定的办法,但有三点须要留神:
- call和apply是立刻执行,bind则是返回一个绑定了this的新函数,只有你调用了这个新函数才真的调用了指标函数
- bind函数存在屡次绑定的问题,如果屡次绑定this,则以第一次为准。
- bind函数实际上是显示绑定(call、apply)的一个变种,称为硬绑定。因为硬绑定是一种十分罕用的模式,所以在 ES5 中提供了内置的办法
Function.prototype.bind
为什么屡次应用bind绑定this,以第一次为准呢?咱们看上面的例子:
function foo() { console.log( this.name );} var obj1 = { name: 'obj1'}; var obj2 = { name: 'obj2'}var fn = foo.bind(obj1).bind(obj2)fn() // => 'obj1'fn.call(obj2) // => 'obj1'
也就是说bind函数只能绑定一次,屡次绑定是没有用的,绑定后的函数this无奈扭转,即便call/apply也不行,所以才称作硬绑定。
但凡事总有例外,且看new绑定。
绑定的优先级
如果显示绑定和new绑定同时存在,或者更宽泛的说:在某个调用地位多条绑定规定同时存在怎么办呢?为了解决这个问题就必须给这些规定设定优先级,这就是咱们接下来要介绍的内容。
毫无疑问,Window绑定的优先级是最低的,显式绑定和隐式绑定的优先级,通过下面的例子也能够证实,显式大于隐式。所以目前程序是:显式 > 隐式 > Window
那咱们来测试下显示绑定和new绑定的优先级程序。因为call/apply无奈和new一起应用,咱们能够应用bind(硬绑定)来验证。
function foo() { this.name = 'Cherry';} var obj = { name: 'obj'}; var fn = foo.bind(obj)var result = new fn()console.log(obj.name) // => 'obj'console.log(result.name) // => 'Cherry'
不言而喻的,new的优先级,大于显示绑定。最终程序为:new > 显式 > 隐式 > Window
。
于是咱们判断this,就有了一个程序:
- 函数是否在new中调用?
- 是否通过call、apply、bind等调用?
- 是否在某个上下文对象中调用?
- 都不是则是Window绑定。且严格模式下绑定到undefined。
小结
- this 的四种绑定形式:隐式绑定、显示绑定、window 绑定、new 绑定
扭转 this 的指向有以下几种办法:
- 箭头函数
- 在函数外部应用 _this = this
- 应用 apply、call、bind
- 应用 new
判断this次要有以下步骤:
- 函数是否在new中调用?
- 是否通过call、apply、bind等调用?
- 是否在某个上下文对象中调用?
- 都不是则是默认绑定。且严格模式下绑定到undefined。
- 绑定优先级:new > 显式 > 隐式 > Window
另外还要留神箭头函数的特殊性、在call/apply中应用undefined和null会被疏忽这一个性、bind的硬绑定以及:this永远指向最初调用它的那个对象。
以上就是对于判断this指向的总结,理解了以上几个关键点,this的指向你曾经很理解啦~
对于
作者齐小神,前端程序媛一枚。
有点文艺,喜爱摄影。 尽管当初朝九晚五,埋头苦学, 但幻想是做女侠,扶贫济穷,仗剑走咫尺。 心愿有一天能改完 BUG 去实现本人的幻想。
公众号:大前端Space,不定时更新,欢送来玩~