子曰:this 是什么?
什么是this
this
不是编写时绑定,而是运行时绑定。它依赖于函数调用的上下文条件。this
绑定与函数声明的位置没有任何关系,而于函数被调用的方式紧密相连。
当一个函数被调用时,会建立一个称为执行环境的活动记录。这个记录包含函数是从何处(调用–call-stak
)被调用的,韩式是如何被调用的,被传递了什么参数等信息。这个记录的属性之一,就是函数执行期间将被使用的this
引用。
this
实际上是在函数被调用时建立的一个绑定,它指向什么是完全由函数被调用的调用点(call-site
)来决定的。
调用栈(call-stack)
使我们到达当前执行位置而被调用的所用方法的堆栈。
调用点(call-site)
- 函数在代码中被调用的位置(不是被声明的位置)。
- 调用点位于当前执行中的函数之前的调用
展示调用栈和调用点:
1 | function baz () { |
判断调用点的四种规则
1. 默认绑定(Default Binding)
独立函数调用
1 | function foo () { |
上述为非严格模式,因此this
为全局对象,而在严格模式下(strict mode
)下,this
为undefined
1 | function foo () { |
2. 隐含绑定(Implicit Binding)
调用点是否有一个环境对象(content object),也称为拥有者(owning)或容器(containing)对象。
1 | function foo () { |
首先foo()
被声明然后作为引用属性添加到obj
。调用点使用obj
环境来 引用 函数,也就是说obj
对象在函数被调用的时间点上“拥有”或“包含” 这个 函数引用。
在foo()
被调用的位置上,它被冠以一个指向obj
的对象引用。当一个方法引用存在一个环境对象时,隐含绑定 规则会说:这个环境对象应当被用于这个函数调用的this
绑定。
因为obj
是foo()
调用的this
,所以this.a
就是obj.a
的同义词。
还要注意:只有对象属性引用链的最后一层是影响调用点的。
隐含丢失
当一个隐含绑定
丢失了它的绑定,它通常会退回默认绑定
,根据strict mode
的状态,其结果不是全局对象就是undefined
。
1 | function foo () { |
尽管bar
似乎是obj.foo
的引用,但实际上它只是另一个foo
本身的引用而已。起作用的调用点是bar()
,因此 默认绑定 适用于这里。
考虑传递一个回调函数:
1 | function foo () { |
参数传递仅仅是一种隐含的赋值,而且因为我们在传递一个函数,它是一个隐含的引用赋值,所以最终结果前一个代码段一样。
如果接受传递回调的函数是语言内建的,也是一样的。
1 | function foo () { |
下面的setTimeout()
实现,等同于JavaScript 环境内建的实现:
1 | function setTimeout(fn, delay) { |
注意:事件处理器强制回调的this
指向触发事件的DOM 元素。
3. 明确绑定(Explicit Binding)
使用函数的call(...)
和apply(..)
方法,强制一个函数调用使用某个特定对象作为this
绑定。它们接收的第一个参数都是一个用于this
的对象,之后使用这个指定的this
来调用函数。因为明确指定this
是什么,所以我们称这种方式为 明确绑定(explicit binding)。
1 | function foo () { |
通过foo.call(..)
使用 明确版定 来调用foo
,允许我们强制函数的this
指向obj
。
靠 明确绑定 不能解决函数“丢失” 自己原本的this
绑定、被第三方框架覆盖等问题。
硬绑定(Hard Binding)
1 | function foo (something) { |
ES5 提供Function.prototype.bind
,像这样使用:
1 | function foo (something) { |
bind(..)
返回一个硬编码的新函数,它使用你指定的this
环境来调用原本的函数。
API 调用的“环境”
许多库中的函数,和许多在JavaScript 语言以及宿主环境中的内建函数,都提供一个可选参数,通常称为“环境(context)”,这种设计作为一种替代方案来确保你的回调函数使用特定的this
而不必非得使用bind(..)
。
1 | function foo (el) { |
3. new
绑定(new
Binding)
new
是函数调用可以绑定this
的最后一种方式,我们称之为 new 绑定(new binding)
1 | function foo (a) { |
通过在前面使用new
来调用foo(..)
,我们构建了一个新的对象并把这个新对象作为foo(..)
调用的this
。
判断this
- 函数是通过
new
被调用的吗(new 绑定)?如果是,this
就是新构建的对象。1
var bar = new foo()
- 函数是通过
call
或apply
被调用(明确绑定),设置是隐藏在bind
硬绑定之中吗?如果是,this
就是那个被明确指定的对象1
var bar = foo.call(obj2)
- 函数是通过环境对象(也称为拥有者或容器对象)被调用的吗(隐含绑定)?如果是,
this
就是那个环境对象。1
var bar = obj1.foo()
- 否则,使用默认的
this
(默认绑定)。如果在strict mode
下,就是undefined
,否则是global
对象。1
var bar = foo()
特殊规则
词法this
ES6 引入了箭头函数(arrow-function),箭头函数不是通过function
关键字声明的,而是通过所谓的“大箭头”操作符:=>
。箭头函数从封闭它的(函数或全局)作用域采用this
绑定。
下面是箭头函数的词法作用域:
1 | function foo () { |
在foo()
中创建的箭头函数在词法上捕获foo()
被调用时的this
,不管它是什么。因为foo()
被this
绑定到obj1
,bar
(被返回的箭头函数的一个引用)也将被this
绑定到obj1
。一个箭头函数的词法绑定是不能被覆盖的(就连new
也不行)。