如何确定this的指向

821 阅读2分钟

this

this 关键字是 JavaScript 中最复杂的机制之一。如果写Js的时候搞不懂this的指向,那会产生各种bug。

由于《你不知道的Js》中写的this实在是太清晰了,因此本文与其说是谈自己理解,倒不如说是笔记。

要判断this的内容,一定要去寻找调用位置,也只有调用位置才能决定this的指向内容。而不是声明的位置。

以下是几个绑定规则:

1. 默认绑定

最常见的函数调用,莫过于独立函数调用了。

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

我们记住一句话:当一个函数是直接使用不带任何修饰的函数引用进行调用的,那么就是默认绑定,函数内的this指向的就是全局对象。

因此上面的代码中打印出来的结果是2。this指向了全局对象,而var a = 2,就是给全局对象声明了一个键为a值为2的键值对,得到的自然就是2。

2. 隐式绑定

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

注意:无论foo是在obj里定义,还是obj外定义,严格意义上来说foo都是不属于obj这个对象的,foo仍然是一个独立的函数。

我们记住一句话: 当一个函数是隶属于某个对象来调用的,那么就会触发隐式绑定规则,函数内的this指向的就是绑定到这个对象的上下文。

因此在obj.foo()时,foo中的this.a与obj.a是一致的。打印出来的同样都是2。

function foo() { 
  console.log( this.a );
}
var obj2 = { 
  a: 42, 
  foo: foo
};
var obj1 = { 
  a: 2, 
  obj2: obj2
};
obj1.obj2.foo(); // 42

对象属性引用链中只有最顶层或者说最后一层会影响调用位置。

隐式丢失

这是隐式绑定最重要的地方,因为这个点最容易被忽略,导致bug产生。

  1. 当使用函数别名时
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这个独立函数,因此它其实触发的是规则1,应用了默认绑定。

  1. 当把函数作为参数传递时
function foo() { 
  console.log( this.a );
}
function doFoo(fn) { // fn 其实引用的是 foo
  fn(); // <-- 调用位置! 
}
var obj = { 
  a: 2, 
  foo: foo
};
var a = "oops, global"; // a 是全局对象的属性
doFoo( obj.foo ); // "oops, global"

函数作为参数传递时,其实就是一种隐式赋值,fn可以理解为上文的函数别名。更加可以理解为函数作为参数传递时,引用的也仅仅是函数的引用,不会将obj.foo的上下文也引入,因此this绑定的仍然是全局对象。

3.显式绑定

如果说调用函数的时候不想在对象内部包含函数引用,而是想在某个对象上强制调用函数。

那就直接用call或者apply即可。

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

总结:

判断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()

箭头函数

function foo() { // 返回一个箭头函数 
  return (a) => { //this 继承自 foo() 
    console.log( this.a );
  }; 
}
var obj1 = { 
  a:2
};
var obj2 = {
  a:3
}; 
var bar = foo.call( obj1 );
bar.call( obj2 ); // 2, 不是 3 !

在这个例子中,可以明确两点:

  1. 箭头函数里的this是根据外层的作用域来决定的。
  2. 箭头函数里的this,是无法再次修改的。