JavaScript中有关this指向随笔

105 阅读4分钟

学习 this 的第一步是明白 this 既不指向函数自身也不指向函数的词法作用域,你也许被 这样的解释误导过,但其实它们都是错误的。this 实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。
在理解 this 的绑定过程之前,首先要理解调用位置:调用位置就是函数在代码中被调用的 位置(而不是声明的位置)。

思考下面的代码:
function foo() { 
    console.log( this.a ); 
}
var obj = { 
    a: 2, 
    foo: foo 
};
obj.foo(); // 2

首先需要注意的是 foo() 的声明方式,及其之后是如何被当作引用属性添加到 obj 中的。
但是无论是直接在 obj 中定义还是先定义再添加为引用属性,这个函数严格来说都不属于 obj 对象。 然而,调用位置会使用 obj 上下文来引用函数,因此你可以说函数被调用时 obj 对象“拥 有”或者“包含”它。
无论你如何称呼这个模式,当 foo() 被调用时,它的落脚点确实指向 obj 对象。当函数引 用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。因为调 用 foo() 时 this 被绑定到 obj,因此 this.a 和 obj.a 是一样的。


思考下面的代码:

function 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() 其实是一个不带任何修饰的函数调用,因此应用了默认绑定。


回调函数丢失 this 绑定是非常常见的。除此之外,还有一种情 况 this 的行为会出乎我们意料:调用回调函数的函数可能会修改 this。
思考下面的代码:

function foo() { 
    console.log( this.a );
}
var obj = {
    a: 2, 
    foo: foo 
};
var a = "oops, global"; // a 是全局对象的属性 
setTimeout( obj.foo, 100 ); // "oops, global

call(..) 和 apply(..) 方法的第一个参数是一个对象,它们会把这个对象绑定到 this,接着在调用函数时指定这个 this。因为你可以直接指定 this 的绑定对象,因此我 们称之为显式绑定。
思考下面的代码:

function foo() { 
    console.log( this.a ); 
}
var obj = {
    a:2 
};
foo.call( obj ); // 2

通过 foo.call(..),我们可以在调用 foo 时强制把它的 this 绑定到 obj 上。
如果你传入了一个原始值(字符串类型、布尔类型或者数字类型)来当作 this 的绑定对 象,这个原始值会被转换成它的对象形式(也就是 new String(..)、new Boolean(..) 或者 new Number(..))。这通常被称为“装箱”。
从 this 绑定的角度来说,call(..) 和 apply(..) 是一样的,它们的区别体现 在其他的参数上,.


ES5 中提供了内置的方法 Function.prototype. bind 。
思考下面的代码:

function foo(something) { 
    console.log( this.a, something ); 
    return this.a + something;
}
var obj = { a:2 };
var bar = foo.bind( obj );
var b = bar( 3 ); // 2 3 
console.log( b ); // 5

bind(..) 会返回一个硬编码的新函数,它会把参数设置为 this 的上下文并调用原始函数。


使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。

  1. 创建(或者说构造)一个全新的对象。
  2. 这个新对象会被执行 [[ 原型 ]] 连接。
  3. 这个新对象会绑定到函数调用的 this。
  4. 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。

思考下面的代码:

function foo(a) {
    this.a = a; 
}
var bar = new foo(2); 
console.log( bar.a ); // 2

使用 new 来调用 foo(..) 时,我们会构造一个新对象并把它绑定到 foo(..) 调用中的 this 上。new 是最后一种可以影响函数调用时 this 绑定行为的方法。


小结

现在我们可以根据优先级来判断函数在某个调用位置应用的是哪条规则。
可以按照下面的 顺序来进行判断:

  1. 函数是否在 new 中调用(new 绑定)?如果是的话 this 绑定的是新创建的对象。 var bar = new foo()
  2. 函数是否通过 call、apply(显式绑定)或者硬绑定调用?如果是的话,this 绑定的是 指定的对象。 var bar = foo.call(obj2)
  3. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上 下文对象。 var bar = obj1.foo()
  4. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined,否则绑定到 全局对象。 var bar = foo() 就是这样。对于正常的函数调用来说,理解了这些知识你就可以明白 this 的绑定原理了。

this词法

箭头函数并不是使用 function 关键字定义的,而是使用被称为“胖箭头”的操作符 => 定 义的。箭头函数不使用 this 的四种标准规则,而是根据外层(函数或者全局)作用域来决 定 this。这 其实和 ES6 之前代码中的 self = this 机制一样
箭头函数可以像 bind(..) 一样确保函数的 this 被绑定到指定对象,此外,其重要性还体 现在它用更常见的词法作用域取代了传统的 this 机制。