原文链接:https://javascript.info/bind,translate with ❤️ by zhangbao.

当使用带有对象方法或传递对象方法的 setTimeout 时,有一个已知的问题:“丢失 this”。

突然,这就停止工作了。这种情况对于新手开发人员来说是很典型的,但是也有经验丰富的开发人员。

丢失“this”

我们已经知道,在 JavaScript 中,很容易会遇到丢失 this 的情况。一旦一个方法脱离对象在某个地方被调用,就会遇到 this 丢失的情况。

下面是 setTimeout 的情况:

  1. let user = {
  2. firstName: "John",
  3. sayHi() {
  4. alert(`Hello, ${this.firstName}!`);
  5. }
  6. };
  7. setTimeout(user.sayHi, 1000); // Hello, undefined!

正如我们所看到的,输出显示的不是“John”(this.firstName),而是 undefiend。

那是因为 setTimeout 得到了函数 user.sayHi,与对象分开。最后一行可以重写为:

  1. let f = user.sayHi;
  2. setTimeout(f, 1000); // lost user context

浏览器中的 setTimeout 方法有点特殊:它为函数调用设置了 this=window(对于 Node.js 来说,这变成了计时器对象,但在这里并不重要)。所以对于 this.firstName 试图获取 ,window.firstName 不存在。在其他类似的情况下,通常 this 是 undefined。

这个任务是非常典型的——我们想要在其他地方(这里——到调度程序)传递一个对象方法,在那里它将被调用。如何确保在正确的上下文环境中调用它?

解决方案 1:包装函数

最简单的解决方案是使用包装函数:

  1. let user = {
  2. firstName: "John",
  3. sayHi() {
  4. alert(`Hello, ${this.firstName}!`);
  5. }
  6. };
  7. setTimeout(function() {
  8. user.sayHi(); // Hello, John!
  9. }, 1000);

现在起作用了,因为它接收来自外部词法环境的 user,然后正常调用该对象上的方法。

相同作用,但是语法更短的:

  1. setTimeout(() => user.sayHi(), 1000); // Hello, John!

看起来很好,但是我们的代码结构中出现了一个小漏洞。

如果在 setTimeout 触发器之前(有一个延迟!)user 值更改了呢?然后,突然,它会调用错误的对象!

  1. let user = {
  2. firstName: "John",
  3. sayHi() {
  4. alert(`Hello, ${this.firstName}!`);
  5. }
  6. };
  7. setTimeout(() => user.sayHi(), 1000);
  8. // ...within 1 second
  9. user = { sayHi() { alert("Another user in setTimeout!"); } };
  10. // Another user in setTimeout?!?

下一个解决方案保证这样的事情不会发生。

解决方案 2:bind

函数提供了一个内置的方法 bind,可以解决 this 问题。

基本语法:

  1. // more complex syntax will be little later
  2. let boundFunc = func.bind(context);

bind(context) 的结果是一个特殊的函数式的“奇异对象”,它可以作为函数调用,并透明地为 func 设置 this=user 的上下文环境。

换句话说,调用 boundFunc 就像给 func 函数绑定了固定的 this 值。

例如,这里的 funcUser 是给 func 传递固定 this=user 的函数:

  1. let user = {
  2. firstName: "John"
  3. };
  4. function func() {
  5. alert(this.firstName);
  6. }
  7. let funcUser = func.bind(user);
  8. funcUser(); // John

这里的 func.bind(user) 是 func 的“绑定版本”,有固定的 this 值(即 user)。

所有的参数都会“如实”的传递给原始函数 func。例如:

  1. let user = {
  2. firstName: "John"
  3. };
  4. function func(phrase) {
  5. alert(phrase + ', ' + this.firstName);
  6. }
  7. // bind this to user
  8. let funcUser = func.bind(user);
  9. funcUser("Hello"); // Hello, John (argument "Hello" is passed, and this=user)

现在让我们尝试一个对象方法:

  1. let user = {
  2. firstName: "John",
  3. sayHi() {
  4. alert(`Hello, ${this.firstName}!`);
  5. }
  6. };
  7. let sayHi = user.sayHi.bind(user); // (*)
  8. sayHi(); // Hello, John!
  9. setTimeout(sayHi, 1000); // Hello, John!

在 (*) 处,我们将方法 user.sayHi 绑定给了 user。sayHi 是一个“绑定”函数,可以单独调用或传递给setTimeout——这都没关系,因为上下文始终是正确的。

这里我们可以看到,参数是通过“如实”传递的,只有 this 是通过 bind 来确定的:

  1. let user = {
  2. firstName: "John",
  3. say(phrase) {
  4. alert(`${phrase}, ${this.firstName}!`);
  5. }
  6. };
  7. let say = user.say.bind(user);
  8. say("Hello"); // Hello, John ("Hello" argument is passed to say)
  9. say("Bye"); // Bye, John ("Bye" is passed to say)

注:便捷方法:bindAll

如果一个对象有很多方法,并且我们计划动态地传递它,那么我们就可以将它们全部绑定在一个循环中:

  1. for (let key in user) {
  2. if (typeof user[key] == 'function') {
  3. user[key] = user[key].bind(user);
  4. }
  5. }

JavaScript 库还提供了方便的聚集绑定的功能,例如,lodash 中的 bindAll(obj)

总结

函数方法 func.bind(context, …args) 返回函数 func 的“绑定变体”,它可以修复上下文和给定的第一个参数。

通常,我们应用 bind 来解决对象方法中的问题,这样我们就可以把它传递到某个地方。例如 setTimeout。在现代发展中有更多的理由与之结合,我们以后会遇到的。

(完)