学习 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();

在知道调用位置之后,判断需要应用如下的四条规则中的哪一条。(多条规则都可用时,注意优先级顺序)

  1. 默认绑定
    首先要介绍的是最常用的函数调用类型:独立函数调用。可以把这条规则看作是无法应用其他规则时的默认规则。

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

    此时 foo() 是直接使用不带任何修饰的函数引用调用,因此只能使用默认绑定,无法应用其他规则。
    如果使用严格模式(strict mode),那么全局对象将无法使用默认绑定,因此 this 会绑定到 undefined。

  2. 隐式绑定
    另一条需要考虑的规则是调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含。

    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 其实是一个不带任何修饰的函数调用,因此应用了默认绑定。
    传参的时候也会发生隐式丢失,同理。

  3. 显示绑定
    就像我们刚才看到的那样,在分析隐式绑定时,我们必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把 this 间接(隐式)绑定到这个对象上。
    那么如果我们不想在对象内部包含函数引用,而想在某个对象上强制调用函数,该怎么做呢?
    JavaScript 提供的绝大多数函数以及你自己创建的所有函数都可以使用 call(..)apply(..) 方法。
    这两个方法是如何工作的呢?它们的第一个参数是一个对象,它们会把这个对象绑定到 this,接着在调用函数时指定这个 this。因为你可以直接指定 this 的绑定对象,因此我们称之为显式绑定。

    function foo() {
      console.log(this.a)
    }
    var obj = {
      a: 2
    };
    foo.call(obj); // 2
  4. new 绑定
    使用 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 绑定行为的方法,我们称之为 new 绑定。