学习 this 的第一步是明白 this 既不指向函数自身也不指向函数的词法作用域。this 实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用,即调用位置。
调用位置:调用位置就是函数在代码中被调用的位置(而不是声明的位置)。
对象属性引用链中只有最顶层或者说最后一层会影响调用位置。(不包括自身)
function a() { // 当前调用栈是: a // 因此,当前调用位置是全局作用域 console.log('a'); b(); } function b() { // 当前调用栈是: a -> b // 因此,当前调用位置在 a 中 console.log('b'); c(); } function c() { // 当前调用栈是: a -> b -> c // 因此,当前调用位置在 b 中 console.log('c'); } a();
在知道调用位置之后,判断需要应用如下的四条规则中的哪一条。(多条规则都可用时,注意优先级顺序)
默认绑定
首先要介绍的是最常用的函数调用类型:独立函数调用。可以把这条规则看作是无法应用其他规则时的默认规则。function foo() { console.log(this.a); } var a = 2; foo(); // 2
此时 foo() 是直接使用不带任何修饰的函数引用调用,因此只能使用默认绑定,无法应用其他规则。
如果使用严格模式(strict mode),那么全局对象将无法使用默认绑定,因此 this 会绑定到 undefined。隐式绑定
另一条需要考虑的规则是调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含。function foo() { console.log(this.a); } var obj = { a: 2, foo: foo } obj.foo() // 2
首先需要注意的是 foo() 的声明方式,及其之后是如何被当作引用属性添加到 obj 中的。但是无论是直接在 obj 中定义还是先定义再添加为引用属性,这个函数严格来说都不属于 obj 对象。
然而,调用位置会使用 obj 上下文来引用函数,因此你可以说函数被调用时 obj 对象“拥有”或者“包含”它。
隐式丢失
一个最常见的 this 绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把 this 绑定到全局对象或者 undefined 上,取决与是否是严格模式。function foo() { console.log(this.a); } var obj = { a: 2, foo: foo } var bar = obj.foo; // 函数别名 var a = 'global'; // a 是全局对象的属性 bar(); // 'global'
此时 bar 虽然是 obj.foo 的一个引用,但是实际上,它引用的是 foo 这个函数本身。
因此 bar 其实是一个不带任何修饰的函数调用,因此应用了默认绑定。
传参的时候也会发生隐式丢失,同理。显示绑定
就像我们刚才看到的那样,在分析隐式绑定时,我们必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把 this 间接(隐式)绑定到这个对象上。
那么如果我们不想在对象内部包含函数引用,而想在某个对象上强制调用函数,该怎么做呢?
JavaScript 提供的绝大多数函数以及你自己创建的所有函数都可以使用 call(..) 和 apply(..) 方法。
这两个方法是如何工作的呢?它们的第一个参数是一个对象,它们会把这个对象绑定到 this,接着在调用函数时指定这个 this。因为你可以直接指定 this 的绑定对象,因此我们称之为显式绑定。function foo() { console.log(this.a) } var obj = { a: 2 }; foo.call(obj); // 2
new 绑定
使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。- 创建(或者说构造)一个全新的对象。
- 这个新对象会被执行 [[ 原型 ]] 连接。
- 这个新对象会绑定到函数调用的 this。
- 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。
function foo(a) { this.a = a; } var bar = new foo(2); console.log( bar.a ); // 2
使用 new 来调用 foo(..) 时,我们会构造一个新对象并把它绑定到 foo(..) 调用中的 this 上。new 是最后一种可以影响函数调用时 this 绑定行为的方法,我们称之为 new 绑定。