this 是 JavaScript 中最复杂的机制之一。它是一个很特别的关键字,被自动定义在所有函数的作用域中。与词法作用域不同,this 是在运行时进行绑定的,并不是在编写时,它的上下文取决于函数调用的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
0.关于 this
关于 this 主要有两种误解,一种是认为 this 指向函数自身,另一种是 this 指向函数的作用域。
0.1 指向自身
思考以下代码:
1 | function foo(num) { |
执行 foo.count = 0 时,的确向函数对象 foo 添加了一个属性 count。但是函数内部代码 this.count 中的 this 并不是指向那个函数对象,所以虽然属性名相同,跟对象却并不相同。
实际上,如果深入探索的话,就会发现这段代码在无意中创建了一个全局变量 count,它的值为 NaN。
如果要让上面的代码实现我们的功能,我们可以用 foo 来代替 this 来引用函数对象:
1 | function foo(num) { |
另一种方法是强制 this 指向 foo 函数对象:
1 | function foo(num) { |
0.2 它的作用域
第二种常见的误解是,this指向函数的作用域。需要明确的是,this在任何情况下都不指向函数的词法作用域。
1 | function foo() { |
因此在学习 this 之前,我们必须明白,this 既不指向函数自身也不指向函数的词法作用域,this 实际上是在函数被调用时发生绑定的。
1.调用位置
在理解this的绑定规则之前,首先要理解调用位置,即函数在代码中被调用的位置。最重要的是要分析调用栈,我们关心的调用位置就是当前正在执行的函数的前一个调用中。
1 | function baz() { |
注意我们是如何分析出真正的调用位置的,因为它决定了 this 的绑定。
2.绑定规则
我们首先需要找到调用位置,然后判断需要应用下面四条规则中的哪一条。首先会介绍四条规则,然后说明多条规则都可以使用时的优先级。
2.1 默认绑定
默认绑定就是简单的独立函数调用,可以把这条规则看作是无法应用其它规则时的默认规则。
1 | function foo() { |
在代码中,foo是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定。在非严格默认下,默认绑定的 this 指向全局对象,严格模式下为 undefined。
2.2 隐式绑定
另一条需要考虑的规则是调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含。
1 | function foo() { |
当 foo 被调用时,它前面加上了对 obj 的引用。当函数引用有上下文对象时,隐式绑定的规则会把函数调用中的 this 绑定到这个上下文对象。
对象属性链中只有上一层或者说最后一层在调用位置中起作用。举例来说:
1 | function foo() { |
隐式丢失
一个最常见的 this 绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把 this 绑定到全局对象或者 undefined 上。
1 | function foo() { |
虽然 bar 是 obj.foo 的一个引用,但是实际上它引用的是 foo 函数本身,因此此时的 bar() 其实是一个不带任何修饰符的函数调用,因此应用了默认绑定。
一种更微妙、更常见并且更出乎意料的情况发生在传入回调函数时:
1 | function foo() { |
传递参数其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值。
同样把函数传入语言内置的函数结果也是一样的。
1 | function foo() { |
经过上面的分析我们知道,回调函数丢失 this 绑定是非常常见的。
2.3 显示绑定
就像我们刚才看到的那样,在分析隐式绑定时,我们必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把 this 间接绑定到对象上。
如果我们不想在对象内部包含函数的引用,而想在某个对象上强制调用函数,这是我们需要使用函数的 call() 和 apply() 方法。
它们的第一个参数是一个对象,是给 this 准备的,接着在调用函数时将其绑定到 this。因为可以直接指定 this 的绑定对象,因此称之为显示绑定。
1 | function foo() { |
显示绑定的另一种情况就是硬绑定。
1 | function foo() { |
因为我们把 bar 函数内部调用了 foo,而 foo 的 this 已经被强制绑定在 obj 上,因此无论之后如何调用 bar 函数,它总会手动在 obj 上调用 foo。
硬绑定的另一种应用场景就是创建一个包裹函数,负责接收参数并返回值:
1 | function foo(something) { |
另一种方法是创建一个可以重复使用的辅助函数:
1 | function foo(something) { |
ES5 中提供了 Function.prototype.bind 函数,它的用法如下:
1 | unction foo(something) { |
bind() 会返回一个硬编码的新函数,它会把你指定的参数设置为 this 的上下文并调用原始函数。
2.4 new绑定
在传统的面向对象的语言中,“构造函数”是类中的一些的特殊方法,使用 new 初始化类时会调用类中的构造函数。Javascript 中也有一个 new 操作符,但是 Javascript 中 new 的机制实际上和面向对象的语言完全不同。在 Javascript 中,构造函数只是一些使用 new 操作符时被调用的函数。它们并不属于某个类,也不会实例化一个类。
使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作:
- 创建一个全新的对象。
- 这个对象会被执行 [[Prototype]] 连接。
- 这个新对象会被绑定到函数调用的 this。
- 如果函数没有返回其它对象,那么 new 表达式中的函数调用会自动返回这个新对象。
思考下面代码:
1 | function foo(a) { |
使用 new 来调用 foo() 时,我们会构造一个新对象并把它绑定到 foo() 调用中的 this 上。 new 是最后一种可以影响函数调用时 this 绑定行为的方法,我们称之为 new 绑定。
3.判断 this
学习了上面四条规则,我们可以根据下面的顺序来判断 this 绑定的对象:
- 函数是否在 new 中调用(new 绑定)?如果是的话,this 绑定的是新创建的对象。
- 函数是否通过 call、apply 显示绑定或者硬绑定?如果是的话,this 绑定的是指定对象。
- 函数是否在某个上下文中调用(隐式绑定)?如果是的话,this 绑定的是那个上下文对象。
- 如果都不是,使用默认绑定。严格模式下绑定到 undefined,否则绑定到全局对象。
4.绑定例外
在某些场景下 this 的绑定行为会出乎意料,你认为应该应用其它绑定规则时,实际上应用的可能是默认绑定的规则。
4.1 被忽略的 this
如果把 null 或者 undefined 作为 this 的绑定对象传入 call、apply 或者 bind,这些值在调用时会被忽略,实际应用的是默认绑定的规则。
1 | function foo() { |
一种常见的做法是使用 apply(…) 来“展开”一个数组,并当作参数传入一个函数。类似地,bind(…)可以对参数进行柯里化,这种方法有时非常有用:
1 | function foo(a ,b) { |
4.2 间接引用
另一个需要注意的是你可能有意或者无意地创建一个函数的”间接引用“,在这种情况下,调用这个函数会应用默认绑定规则。
间接引用最容易在赋值的时候发生:
1 | function foo() { |
赋值表达式 p.foo = o.foo 的返回值是目标函数的引用,因此调用位置是 foo() 而不是 p.foo() 或者 o.foo()。
5.this 词法
我们之前介绍的四条规则已经可以包含所有的正常函数。但是在 ES6 中介绍了一种无法使用这些规则的特殊类型函数:箭头函数。
箭头函数不使用 this 的四种标准规则,而是根据外层作用域来决定 this。
我们来看看箭头函数的词法作用域:
1 | function foo() { |
对比正常的函数:
1 | function foo() { |
foo() 内部的箭头函数会捕获调用时 foo() 的 this。由于 foo() 的 this 绑定到 obj1,bar 引用箭头函数的 this 也会绑定到 obj1,箭头函数的绑定无法修改。(new 也不行)
箭头函数最常用于回调函数,例如事件处理器或者定时器:
1 | function foo() { |