前言
this
是JavaScript
里面老生常谈的问题,因为其诡异的机制,基本每个初学者都会迷惑于this
的指向问题,而this
又是面向对象编程的基础,所以弄明白this
的指向是提高自己编程能力的重要一环。《你不知道的JavaScript》上册第二部分详细讲解了this
的几种指向问题,如果你还不懂this
的指向,下面的文章也许能帮你解决这个难题。
this解析
调用位置
- 什么是调用位置?
调用位置就是函数在代码中被调用的位置(而不是声明的位置)。
要分析调用栈(就是为了到达当前执行位置所调用的所有函数)。
什么是调用栈和调用位置:
1 | function baz() { |
绑定规则
默认绑定
首先要介绍的是最常用的函数调用类型:独立函数调用。
1 | function foo() { |
- 声明在全局作用域中的变量(比如
var a = 2
)就是全局对象的一个同名属性。等价于window.a = 2
- 它们本质上就是同一个东西,并不是通过复制得到的,就像一个硬币的两面一样。
当调用 foo()
时, this.a
被解析成了全局变量 a
。为什么?因为在本例中,函数调用时应用了 this
的默认绑定,因此 this
指向全局对象。
那么我们怎么知道这里应用了默认绑定呢?
可以通过分析调用位置来看看
foo()
是如何调用的。在代码中,foo()
是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,无法应用其他规则。
严格模式下,this
会绑定到undefined
1 | function foo() { |
注意一个细节
虽然 this
的绑定规则完全取决于调用位置,但是只有 foo()
运行在非 strict mode
下时,默认绑定才能绑定到全局对象;严格模式下与 foo()
的调用位置无关:
1 | function foo() { |
隐式绑定
思考下面的代码:
1 | function foo() { |
- 无论是直接在
obj
中定义还是先定义再添加为引用属性,foo()
这个函数严格来说都不属于obj
对象。 - 然而,调用位置会使用
obj
上下文来引用函数,当foo()
被调用时,它的落脚点确实指向obj
对象。
当函数引用有上下文对象时,隐式绑定规则会把函数调用中的
this
绑定到这个上下文对象。因为调用foo()
时this
被绑定到obj
,因此this.a
和obj.a
是一样的。
对象属性引用链中只有最顶层或者说最后一层会影响调用位置。举例来说:
1 | function foo() { |
隐式丢失
一个最常见的 this
绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把 this
绑定到全局对象或者 undefined
上,取决于是否是严格模式。1
2
3
4
5
6
7
8
9
10function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // 函数别名!
var a = "oops, global"; // a 是全局对象的属性
bar(); // "oops, global"
虽然 bar
是 obj.foo
的一个引用,但是实际上,它引用的是 foo
函数本身,因此此时的 bar()
其实是一个不带任何修饰的函数调用,因此应用了默认绑定。
1 | function foo() { |
参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值,所以结果和上一个例子一样。
如果把函数传入语言内置的函数而不是传入你自己声明的函数,会发生什么呢?结果是一样的,没有区别:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // a 是全局对象的属性
setTimeout( obj.foo, 100 ); // "oops, global"
//JavaScript 环境中内置的 setTimeout() 函数实现和下面的伪代码类似:
function setTimeout(fn,delay) {
// 等待 delay 毫秒
fn(); // <-- 调用位置!
}
回调函数丢失
this
绑定是非常常见的。调用回调函数的函数也可能会修改this
。
显示绑定
如果我们不想在对象内部包含函数引用,而想在某个对象上强制调用函数,该怎么做呢?
- JavaScript 提供的绝大多数函数以及你自己创建的所有函数都可以使用 call(..) 和 apply(..) 方法。
- 它们的第一个参数是一个对象,它们会把这个对象绑定到this ,接着在调用函数时指定这个 this 。因为你可以直接指定 this 的绑定对象,因此我们称之为显式绑定。
1 | function foo() { |
通过 foo.call(..)
,我们可以在调用 foo
时强制把它的 this
绑定到 obj
上。
如果你传入了一个原始值(字符串类型、布尔类型或者数字类型)来当作 this 的绑定对象,这个原始值会被转换成它的对象形式(也就是 new String(..) 、 new Boolean(..)或者new Number(..) )。这通常被称为“装箱”。
从 this
绑定的角度来说, call(..)
和 apply(..)
是一样的,它们的区别体现在其他的参数上:
apply
最多只能有两个参:新this
对象和一个数组Array
。call
它可以接受多个参数,第一个参数也是新this
对象,后面则是多个参数。
硬绑定
1 | function foo() { |
我们创建了函数 bar()
,并在它的内部手动调用了 foo.call(obj)
,因此强制把 foo
的 this
绑定到了 obj
。无论之后如何调用函数 bar
,它总会手动在 obj
上调用 foo
。这种显示强制绑定,我们称之为硬绑定。
由于硬绑定十分常用,所以在 ES5
中提供了内置的方法 Function.prototype.bind
:
1 | function foo(something) { |
bind(..)
会返回一个硬编码的新函数,它会把参数设置为 this
的上下文并调用原始函数。
API调用的“上下文”
第三方库的许多函数,以及 JavaScript
语言和宿主环境中许多新的内置函数,都提供了一个可选的参数,通常被称为“上下文”(context),其作用和 bind(..)
一样,确保你的回调函数使用指定的 this
。
1 | function foo(el) { |
这些函数实际上就是通过 call(..)
或者 apply(..)
实现了显式绑定。
new绑定
先了解一个事实,JavaScript
中 new
的机制实际上和面向类的语言完全不同。
- 在 JavaScript 中,构造函数只是一些使用 new 操作符时被调用的函数。它们并不会属于某个类,也不会实例化一个类。实际上,它们甚至都不能说是一种特殊的函数类型,它们只是被 new 操作符调用的普通函数而已。
- 实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”。
使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。
- 创建(或者说构造)一个全新的对象。
- 这个新对象会被执行 [[ 原型 ]] 连接。
- 这个新对象会绑定到函数调用的 this 。
- 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。
1 | function foo(a) { |
使用 new
来调用 foo(..)
时,我们会构造一个新对象并把它绑定到 foo(..)
调用中的 this
上。 new
是最后一种可以影响函数调用时 this
绑定行为的方法,我们称之为 new
绑定。
ES6中的this词法
ES6 中介绍了一种无法使用上述四种规则的特殊函数类型:箭头函数。
箭头函数不使用 this
的四种标准规则,而是根据外层(函数或者全局)作用域来决定 this
。
1 | function foo() { |
foo()
内部创建的箭头函数会捕获调用时 foo()
的 this
。由于 foo()
的 this
绑定到 obj1
,bar
(引用箭头函数)的 this
也会绑定到 obj1
,箭头函数的绑定无法被修改。( new
也不行!)
箭头函数最常用于回调函数中,例如事件处理器或者定时器:
1 | function foo() { |
箭头函数可以像 bind(..)
一样确保函数的 this
被绑定到指定对象,此外,其重要性还体现在它用更常见的词法作用域取代了传统的 this
机制。实际上,在 ES6
之前我们就已经在使用一种几乎和箭头函数完全一样的模式。
1 | function foo() { |
实际工作中,为了确保编程风格统一,便于后期维护,你或许应当:
- 只使用词法作用域并完全抛弃错误
this
风格的代码; - 完全采用
this
风格,在必要时使用bind(..)
,尽量避免使用self = this
和箭头函数。
优先级
显示绑定>隐式绑定
1 | function foo() { |
new绑定>隐式绑定
1 | function foo(something) { |
new绑定>显示绑定
1 | function foo(something) { |
至此,我们可以根据优先级来判断函数在某个调用位置应用的是哪条规则。
- 函数是否在 new 中调用( new 绑定)?如果是的话 this 绑定的是新创建的对象。
var bar = new foo()
- 函数是否通过 call 、 apply (显式绑定)或者硬绑定调用?如果是的话, this 绑定的是指定的对象。
var bar = foo.call(obj2)
- 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话, this 绑定的是那个上下文对象。
var bar = obj1.foo()
- 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined ,否则绑定到全局对象。
var bar = foo()
总结
判断this绑定对象的四条规则:
- 由 new 调用?绑定到新创建的对象。
- 由 call 或者 apply (或者 bind )调用?绑定到指定的对象。
- 由上下文对象调用?绑定到那个上下文对象。
- 默认:在严格模式下绑定到 undefined ,否则绑定到全局对象。
四种绑定规则的优先级:new绑定>显式绑定>隐式绑定>默认绑定
例外
ES6 中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定this ,具体来说,箭头函数会继承外层函数调用的 this 绑定(无论 this 绑定到什么)。这其实和 ES6 之前代码中的 self = this 机制一样。
额外说明
默认绑定
this
的指向,是在函数被调用的时候确定的- 如果函数独立调用,那么该函数内部的
this
,在严格模式中指向undefined
。在非严格模式中,指向全局对象。- 例如
fn()
是独立调用,fn函数
里面的this
在非严格模式下指向全局对象,严格模式下会指向undefined
。
隐式绑定
- 如果函数被某个对象调用,那么函数中的
this
,指向的就是这个对象。- 例如
obj.fn()
,fn()
被obj
调用,this
指向的就是obj
。- 如果出现
obj1.obj2.foo();
这种情况,this
指向的是obj2
,对象属性引用链中只有最顶层或者说最后一层会影响调用位置。
关于属性调用,比如obj.a
- 当
obj
在全局环境中声明时,无论obj.a
在什么地方调用,这里的this
都指向全局对象window
。- 当
obj
在函数环境中声明时,在严格模式下this
指向undefined
,在非严格模式下指向全局对象。