深圳

探究 JavaScript 中的 this

关于 this 的误解

  • this 并不指向函数自身
  • 也并不指向函数的作用域

⭐️ 正确的认识:this 实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。

this 详解

调用位置

要寻找函数被调用的位置看似简单, 但是做起来并没有这么简单, 因为某些编程模式可能会隐藏真正的调用位置1

最重要的是要分析调用栈(就是为了到达当前执行位置所调用的所有函数)。我们关心的调用位置就在当前正在执行的函数的前一个调用中。如果你想要分析 this 的绑定,使用开发者工具得到调用栈,然后找到栈中第二个元素,这就是真正的调用位置。

上面这段话的解释太抽象了🤷‍♂️,后面我们结合实际的代码来看

绑定规则

默认绑定

最常用的函数调用类型:独立函数调用。可以把这条规则看作是无法应用其他规则时的默认规则。

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

隐式绑定

调用位置是否有上下文对象,或者说是否被某个对象「拥有」或者「包含」(不过这种说法可能会造成一些误导)

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

当函数引用有上下文对象时, 隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。

隐式丢失

一个最常见的 this 绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定, 从而把 this 绑定到全局对象或者 undefined 上,取决于是否是严格模式。

  • 函数别名。它引用的是 foo 函数本身, 因此此时的 bar() 其实是一个不带任何修饰的函数调用,因此应用了默认绑定。

    function foo() {
    console.log(this.a)
    }
    obj = {
    a: 2,
    foo: foo
    }
    var a = 'oops, global'
    bar = obj.foo // 函数别名
    bar() // 'oops, global'
    
  • 作为参数传递传入回调函数时。参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值,所以结果和上一个例子一样。

    function foo() {
      console.log(this.a)
    }
    
    function doFoo(fn) {
      //fn 其实引用的是 foo
      fn()
    }
    obj = {
      a: 2,
      foo: foo
    }
    var a = 'oops, global'
    
    doFoo(obj.foo) //oops, global
    

显示绑定

我们不想在对象内部包含函数引用,而想在某个对象上强制调用函数。可以使用函数的 call(...)apply(...) 方法,我们把这叫做显式绑定。

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

硬绑定

var a = 0;

function foo() {
  console.log(this.a);
}
var obj = {
  a: 2
};
var bar = function() {
  foo.call(obj);
}
bar(); //2
setTimeout(bar, 100); //2
bar.call(window); //2

并在它的内部手动调用了 foo.call(obj) ,因此强制把 foo 的 this 绑定到了obj 。无论之后如何调用函数 bar ,它总会手动在 obj 上调用 foo 。这种绑定是一种显式的强制绑定,因此我们称之为硬绑定。

bind 方法

由于硬绑定是一种非常常用的模式,所以 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)
bar(3)

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

new 绑定

在 JavaScript 中,构造函数只是一些 使用 new 操作符时被调用的函数。它们并不会属于某个类,也不会实例化一个类。实际上,它们甚至都不能说是一种特殊的函数类型,它们只是被 new 操作符调用的普通函数而已。

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

  1. 创建(或者说构造)一个全新的对象。
  2. 这个新对象会被执行[[Prototype]]连接。
  3. 这个新对象会绑定到函数调用的 this
  4. 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。
function foo(a){
this.a = a;
}
var bar = new foo(2)
console.log(bar.a) // 2

参考链接


  1. 比如在 React 中给元素绑定 onClick 事件传入的函数