原文链接:https://javascript.info/bind,translate with ❤️ by zhangbao.
当使用带有对象方法或传递对象方法的 setTimeout 时,有一个已知的问题:“丢失 this”。
突然,这就停止工作了。这种情况对于新手开发人员来说是很典型的,但是也有经验丰富的开发人员。
丢失“this”
我们已经知道,在 JavaScript 中,很容易会遇到丢失 this 的情况。一旦一个方法脱离对象在某个地方被调用,就会遇到 this 丢失的情况。
下面是 setTimeout 的情况:
let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
setTimeout(user.sayHi, 1000); // Hello, undefined!
正如我们所看到的,输出显示的不是“John”(this.firstName),而是 undefiend。
那是因为 setTimeout 得到了函数 user.sayHi,与对象分开。最后一行可以重写为:
let f = user.sayHi;
setTimeout(f, 1000); // lost user context
浏览器中的 setTimeout 方法有点特殊:它为函数调用设置了 this=window(对于 Node.js 来说,这变成了计时器对象,但在这里并不重要)。所以对于 this.firstName 试图获取 ,window.firstName 不存在。在其他类似的情况下,通常 this 是 undefined。
这个任务是非常典型的——我们想要在其他地方(这里——到调度程序)传递一个对象方法,在那里它将被调用。如何确保在正确的上下文环境中调用它?
解决方案 1:包装函数
最简单的解决方案是使用包装函数:
let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
setTimeout(function() {
user.sayHi(); // Hello, John!
}, 1000);
现在起作用了,因为它接收来自外部词法环境的 user,然后正常调用该对象上的方法。
相同作用,但是语法更短的:
setTimeout(() => user.sayHi(), 1000); // Hello, John!
看起来很好,但是我们的代码结构中出现了一个小漏洞。
如果在 setTimeout 触发器之前(有一个延迟!)user 值更改了呢?然后,突然,它会调用错误的对象!
let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
setTimeout(() => user.sayHi(), 1000);
// ...within 1 second
user = { sayHi() { alert("Another user in setTimeout!"); } };
// Another user in setTimeout?!?
下一个解决方案保证这样的事情不会发生。
解决方案 2:bind
函数提供了一个内置的方法 bind,可以解决 this 问题。
基本语法:
// more complex syntax will be little later
let boundFunc = func.bind(context);
bind(context) 的结果是一个特殊的函数式的“奇异对象”,它可以作为函数调用,并透明地为 func 设置 this=user 的上下文环境。
换句话说,调用 boundFunc 就像给 func 函数绑定了固定的 this 值。
例如,这里的 funcUser 是给 func 传递固定 this=user 的函数:
let user = {
firstName: "John"
};
function func() {
alert(this.firstName);
}
let funcUser = func.bind(user);
funcUser(); // John
这里的 func.bind(user) 是 func 的“绑定版本”,有固定的 this 值(即 user)。
所有的参数都会“如实”的传递给原始函数 func。例如:
let user = {
firstName: "John"
};
function func(phrase) {
alert(phrase + ', ' + this.firstName);
}
// bind this to user
let funcUser = func.bind(user);
funcUser("Hello"); // Hello, John (argument "Hello" is passed, and this=user)
现在让我们尝试一个对象方法:
let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
let sayHi = user.sayHi.bind(user); // (*)
sayHi(); // Hello, John!
setTimeout(sayHi, 1000); // Hello, John!
在 (*) 处,我们将方法 user.sayHi 绑定给了 user。sayHi 是一个“绑定”函数,可以单独调用或传递给setTimeout——这都没关系,因为上下文始终是正确的。
这里我们可以看到,参数是通过“如实”传递的,只有 this 是通过 bind 来确定的:
let user = {
firstName: "John",
say(phrase) {
alert(`${phrase}, ${this.firstName}!`);
}
};
let say = user.say.bind(user);
say("Hello"); // Hello, John ("Hello" argument is passed to say)
say("Bye"); // Bye, John ("Bye" is passed to say)
注:便捷方法:bindAll
如果一个对象有很多方法,并且我们计划动态地传递它,那么我们就可以将它们全部绑定在一个循环中:
for (let key in user) {
if (typeof user[key] == 'function') {
user[key] = user[key].bind(user);
}
}
JavaScript 库还提供了方便的聚集绑定的功能,例如,lodash 中的 bindAll(obj)。
总结
函数方法 func.bind(context, …args) 返回函数 func 的“绑定变体”,它可以修复上下文和给定的第一个参数。
通常,我们应用 bind 来解决对象方法中的问题,这样我们就可以把它传递到某个地方。例如 setTimeout。在现代发展中有更多的理由与之结合,我们以后会遇到的。
(完)