JavaScript this 指南

深入理解 JavaScript 中的 this

从调用点出发,讲清楚 JavaScript 如何决定 this 指向——默认绑定、隐式绑定、显式绑定与 new 绑定,配有可运行示例。

this 不取决于函数写在哪里。它在每次调用时都会重新决定,完全取决于这次调用所用的语法。同一个函数每次运行都可能得到不同的 this。

这篇文章按照你应该检查的顺序,依次讲解各条绑定规则,最后讲箭头函数——唯一一种 this 表现完全不同的情况。

一句话回答

  • 每次调用时只问一个问题:函数名前面紧跟着的是什么?这个答案就决定了谁绑定了 this。
  • call 和 apply 会立即以显式指定的 this 调用函数;bind 不会调用任何东西,只是返回一个永久绑定了该 this 的新函数。
  • new 绑定的优先级高于以上所有情况:用 new 构造总会创建一个全新对象并把 this 绑定到它上面。

1. 默认绑定:函数名前面没有任何对象

像 greet() 这样的裸调用不提供任何接收者。非严格模式下 this 会回退到全局对象;严格模式下则保持 undefined。这是最容易让人意外的一条规则,因为即使函数原本是从某个对象上取出来的,这条规则依然生效。

裸调用前面什么都没有,因此默认绑定生效

function greet() {
  console.log(this);
}

greet(); // 非严格模式下是全局对象,严格模式下是 undefined

2. 隐式绑定:调用前面紧跟着一个对象

obj.method() 会把 obj 作为 this 提供出来——这就是隐式绑定,也是日常代码最依赖的一条规则。

一旦这个方法被复制到一个裸变量里、并在没有对象在前的情况下被调用,隐式绑定就不再适用,取而代之的是默认绑定。

同一个 greet 函数,被拆下来后就丢失了接收者

const person = {
  name: "Ada",
  greet() {
    console.log(this.name);
  },
};

person.greet(); // "Ada" —— 隐式绑定

const greet = person.greet;
greet(); // undefined —— 默认绑定,接收者已经丢失

3. 显式绑定:call、apply 和 bind

call 和 apply 都可以让你直接指定 this 并立即调用函数;它们的区别只在于剩余参数的传递方式(逐个传入 vs 以数组形式传入)。

bind 不会调用任何东西。它会返回一个全新的函数,并把 this 永久锁定为你提供的值——之后即使用不同的接收者调用这个返回的函数也不会有任何效果。

call 立即调用;bind 只返回一个新函数

function show() {
  console.log(this.id);
}

const target = { id: 42 };

show.call(target); // 42
const bound = show.bind(target);
bound(); // 42 —— 已经锁定,之后无论怎么调用 bound 都不会变

4. new 绑定的优先级高于以上所有规则

用 new 调用一个函数会触发 Construct 操作:创建一个全新对象,把它链接到函数的 prototype 上,并在这次调用中把它绑定为 this——无论这个函数在其他情况下本来会如何解析 this。

new 总会把 this 绑定到新创建的实例上

function Person(name) {
  this.name = name;
}

const ada = new Person("Ada");
console.log(ada.name); // "Ada"

5. 箭头函数不遵循以上任何一条规则

箭头函数没有属于自己的绑定规则。它会在创建时永久捕获外层作用域当时已经存在的 this——call、apply、bind,甚至 new 都无法覆盖它。

这让箭头函数的表现更像是“捕获了 this 的闭包”,而不是“拥有自己绑定规则的函数”——具体是如何体现的,可以参考闭包指南以及 this + 闭包可视化器。

亲自试一试追踪一次 this 绑定的决策过程逐步执行一次调用,观察究竟是默认绑定、隐式绑定、显式绑定还是 new 绑定决定了 this。