在 Java 中,this 关键字指向的是当前对象的引用,对于熟悉 Java 的人来说是再简单不过的东西,因此我一度以为 JavaScript 也是如此,直到有个前辈对我讲起 JavaScript 中 this 的引用问题,他问了几个 this 的问题,我一个也没回答对,显然我对 JS 的了解还只停留在皮毛。
1. this
与我以往接触的 Java 和 C++ 相比,this
关键字在 JavaScript 稍有不同,并且严格模式与非严格模式也存在一定差异,在ES6的箭头函数中,this
的指向又会有一定差异,导致一些菜鸟接触 JavaScript 的 this
关键字时很容易搞混 this
绑定的对象。
this
指向什么,并不单纯取决于在哪个对象中被调用,而是完全取决于在什么地方以什么方式调用
2. this 绑定规则
通过参考网上的资料,this
的绑定规则大致分为 4 种:
- 默认绑定
- 隐形绑定
- 显性绑定
- new 绑定
优先级从低到高。
先看看在全局环境下 this
的指向:
1 | console.log(this) //Window |
结果显示在不进行任何函数的调用时,this
所指向的 Window 对象
2.1 默认绑定
1 | function aaa(){ |
像aaa()
这样直接调用的,使用默认绑定规则,将全局对象 Window 绑定到this
上,所以最后的输出结果为Window
在严格模式下,全局对象无法进行默认绑定,因此在相同的调用方式下会出现 undefined 的情况:
1 | function aaa(){ |
2.2 隐性绑定
看下面一段代码:
1 | function aaa(){ |
由于第一次是直接调用aaa()
,所以使用的是默认绑定规则,this
指向的是Window,而并没有定义全局变量n
,所以输出undefined
第二次调用时,aaa()
函数被当作引用属性,添加到obj
对象上,此时函数aaa()
有了上下文对象,即obj
。此时,函数里的this
默认绑定为上下文对象,最后等于打印obj.n
,因此输出999
。
2.2.1 多层调用链
如果是链性关系,如xx.yy.obj.aaa()
,上下文取函数的直接上级,即紧挨着的那个。
1 | function aaa (){ |
2.2.2 隐式丢失(函数别名)
1 | function aaa (){ |
将obj.aaa
赋值给bar
,调用bar
却没有触发隐式绑定,而是触发了默认绑定,这是为什么?
原因是obj.aaa
是引用属性,赋值给bar
的是aaa
函数本身,所以bar
实际上是aaa
的一个别名,我们通过bar
来找到aaa
,直接调用bar
函数导致触发了默认绑定。
2.2.3 隐式丢失(回调函数)
1 | function aaa() { |
明明在obj
中进行了隐式绑定,为什么最后this还是指向了window
呢?
原因是虽然传入的参数是obj.aaa
,但是因为obj.aaa
是引用属性,所以实际上传入的参数是aaa
函数本身,与obj
没有关系,所以最后触发的依旧是默认绑定
2.3 显性绑定
若要使用上面的隐性绑定,则上下文必须包含我们的函数,但实际中若要每个对象都包含这个函数,会使得维护性变差。在下面的显性绑定中,会给函数强制性绑定this
。
1 | function aaa(){ |
call()
、apply
、bind()
三者比较类似,都是用来改变函数的this
指向,三个函数的第一个参数都是this
所指向的对象,后面的参数是函数的参数。三者aaa()
函数的this
指向obj
对象,所以aaa
中打印的this.n
实际上是obj
对象中的n
。
call()
和apply()
在改变函数的this
的同时会直接调用函数;而bind()
比较特殊,它不会立刻执行,只是将一个值绑定到this
上,并将绑定好的函数返回;当在bind()
后面再加一个括号时,才会立即执行。
在显示绑定中,对于 null 和 undefined 的绑定将不会生效。
1 | function aaa (){ |
2.4.1 硬绑定
1 | function aaa() { |
第一次调用setTimeout
函数,由于在bar
中使用了显示绑定,所以this
绑定了obj1
第二次调用时虽然将bar
显式调用到obj2
上,但是aaa
已经被显示绑定到obj1
上,所以在aaa
中,this
指向obj1
,并不会因为bar
函数内指向obj2
而改变。
2.4 new 绑定
2.4.1 关键字 new
在 javascript 中,关键字 new
与在 Java、C++ 等语言中的作用一样,都是创建一个新对象,但是创建的机制却有所不同。
在其他的一些面向对象的语言中,创建一个新对象总是少不了构造函数这个概念,要创建对象时使用 new ClassName()
的形式自动调用构造函数,而在JS中则有些不同。
js 中只要是用 new
关键字修饰的函数就是构造函数。那么在调用构造函数后,js帮我们做了什么呢:
- 创建一个新对象;[ var obj = {} ]
- 把这个新对象的
__proto__
属性指向 原函数的prototype
属性。(即继承原函数的原型) - 将这个新对象绑定到 此函数的this上
- 返回新对象,如果这个函数没有返回其他对象。
2.4.2 new 绑定
1 | function aaa(){ |
第一次直接调用aaa()
函数,默认绑定Window对象,此时this.n
等同于window.n
;后面以aaa()
函数作为obj
的构造函数,此时输出的this
为aaa
函数对象,此时的this.n
等同于obj.n
。
2.5 绑定优先级
1 | new 绑定 > 显示绑定 > 隐式绑定 > 默认绑定 |
2.6 箭头函数的 this 绑定
在ES6 中,通过“=>
” 而非 function
创建的函数,叫做箭头函数。它的 this
绑定取决于外层(函数或全局)作用域。并且四种绑定规则对箭头函数并不生效。
1 | var aaa = (()=>console.log(this.n)) |
1 | var obj = { |
我们发现同样的箭头函数,在aaa
中额外套了一层function
,而在bbb
中没有,使得两次调用得到的this
是不一样的。
- 我们先要搞清楚一点,
obj
的当前作用域是window
。 - 如果不用
function
(function
有自己的函数作用域)将其包裹起来,那么默认绑定的父级作用域就是window
。 - 用
function
包裹的目的就是将箭头函数绑定到当前的对象上。函数的作用域是当前这个对象,然后箭头函数会自动绑定函数所在作用域的this
,即obj
。