this关键字是JavaScript
中最复杂的机制之一,是一个特别的关键字,被自动定义在所有函数的作用域中,但是相信很多JavaScript
开发者并不是非常清楚它究竟指向的是什么。听说你很懂this,是真的吗?
普通函数
函数的this
指向在函数定义的时候是不能确定的。只有在函数执行的时候才能确定函数的this
指向谁
总结普通函数的绑定规则主要分为以下这几种:
默认绑定
默认绑定是在不能应用其他绑定规则的时候使用的默认规则, 通常是独立函数调用。
例子1:
1 | function getName() { |
在调用getName
的时候,使用了默认绑定,this
指向了全局对象。严格模式下,this
指向undefined
,``undefined`上没有this对象,会抛出错误。
这里例子是window
调用了独立的函数,this
就是指向window
(非严格模式就是windom
)
这里的例子可以这么看:
1 | function getName() { |
隐式绑定
函数的调用时在某个对象上触发的。即调用位置上存在上下文对象。典型的形式为 XXX.fun()
。
例子2:
1 | function getName() { |
是对象person
调用函数logName
, 所以this
指向person
。
getName
函数在外部声明, 严格来说并不属于person
,但是在调用getName
时,调用位置会使用person
的上下文来引用函数,隐式绑定会把函数调用中的this
(即此例getName
函数中的this)绑定到这个上下文对象(即此例中的person
).
注意:对象属性链中只有最后一层会影响到调用位置
例子3:
1 | function getName() { |
因为只有最后一层会确定this
指向的是什么,不管有多少层,在判断this的时候,我们只关注最后一层,即此处的person
。
隐式绑定有一个很大的缺陷,就是很容易丢失(即容易给我们造成误导,我们以为this
指向的是什么,但是实际上并非如此)
例子4:
1 |
|
为什么结果是 shuliqi
?
这是因为logName
直接指向了getName
的引用, 跟person
没关系。针对此类问题,我建议大家只需牢牢继续这个格式:XXX.fn();fn()
前如果什么都没有,那么肯定不是隐式绑定。
除了上面这种丢失之外,隐式绑定的丢失是发生在回调函数中(事件回调也是其中一种),我们来看下面一个例子:
1 | function sayHi(){ |
结果为:
1 | Hello, Wiliam |
第一条输出很容易理解,
setTimeout
的回调函数中,this
使用的是默认绑定,非严格模式下,执行的是全局对象第二条输出是不是有点迷惑了?说好
XXX.fun()
的时候,fun
中的this
指向的是XXX
呢,为什么这次却不是这样了!Why
?其实这里我们可以这样理解:
setTimeout(fn,delay){ fn(); }
,相当于是将person2.sayHi
赋值给了一个变量,最后执行了变量,这个时候,sayHi
中的this
显然和person2
就没有关系了。第三条虽然也是在
setTimeout
的回调中,但是我们可以看出,这是执行的是person2.sayHi()
使用的是隐式绑定,因此这是this
指向的是person2
,跟当前的作用域没有任何关系。
显示绑定
显示绑定就是通过apply
,call
,bind
的方法, 显示的指定this
所指的对象。
apply
,call
,bind
的第一个参数都是该函数this所指的对象。 call
,apply
是一样的, 都会立即执行,只是传参方式不同。call
和apply
都会执行对应的函数,而bind
方法不会。apply
的第二个参数是一个数组。
1 | function sayHi(){ |
结果:Hello, YvetteLau
。因为明确将this
绑定在了person
上
那么,使用了硬绑定,是不是意味着不会出现隐式绑定所遇到的绑定丢失呢?显然不是这样的,不信,继续往下看。
1 | function sayHi(){ |
输出的结果是 Hello, Wiliam.
原因很简单,Hi.call(person, person.sayHi)
的确是将this
绑定到Hi中的this
了。但是在执行fn
的时候,相当于直接调用了sayHi
方法(记住: person.sayH
i已经被赋值给fn
了,隐式绑定也丢了),没有指定this
的值,对应的是默认绑定。
如果我们现在希望绑定不要丢失, 我们该怎么做? 很简单, 在调用fn
的时候也是用显示绑定。
1 | function sayHi(){ |
此时,输出的结果为: Hello, YvetteLau
,因为person
被绑定到H
i函数中的this
上,fn又将这个对象绑定给了sayHi
的函数。这时,sayHi
中的this
指向的就是person
对象。
如果我们将null
或者是undefined
作为this
的绑定对象传入call
、apply
或者是bind
,这些值在调用时会被忽略,实际应用的是默认绑定规则。
1 | function getName() { |
new 绑定
new
的原理:
- 创建一个空对象
- 将空对象的原型指向构造函数的原型属性,从而继承构造原型上的方法(
newObj.__proto__ === Fn.prototype
) - 将构造函数的
this
指向新建的对象,并且执行构造函数的代码,从而获得构造函数的私有属性 - 最后看构造函数是否是返回一个对象,如果是直接返回该对象,如果不是, 则返回我们新建对象
1 | function Fn() { |
这里newFn
对象子所以可以点name
出来,是因为new
关键字可以改变this
指向。
实现new:
1 |
|
测试:
1 | function People(name) { |
普通函数的绑定优先级
我们知道了this有四种绑定规则,但是如果同时应用了多种规则,怎么办?
显然,我们需要了解哪一种绑定方式的优先级更高,这四种绑定的优先级为:
new
绑定 > 显式绑定 > 隐式绑定 > 默认绑定
箭头函数
箭头函数this
的定义: 箭头函数的this
在定义的时候绑定。而不是在执行的时候绑定。它的this
指向在定义的时候继承自外层第一个普通函数的this
。
1 | var person = { |
内层箭头函数logName
本身并没有this
对象。它的this
对象来自于外层作用域。logName
函数的外层函数getName
是一个普通的函数。 它是有this
值的指向person
对象。所以logName
函数的this
指向person
对象。