函数绑定
和对象方法或者和传递对象方法一起使用 setTimeout 时,有一个很常见的问题:“this 丢失”。
突然,this 就停止正常运作了。这种情况在开发的初学者中很典型,但有时也会出现在有经验开发者的代码中。
丢失 “this”
我们已经知道,在 JavaScript 中,this 很容易就会丢失。一旦一个方法被传递到另一个与对象分离的地方 —— this 就丢失了。
下面是使用 setTimeout 时 this 时如何丢失的:
let user = {
firstName: "John",
sayHi() {
console.log(`Hello, ${this.firstName}!`);
}
};
setTimeout(user.sayHi, 1000); // Hello, undefined!
正如我们看到的那样,this.firstName 不是输出为 “John”,而是 undefined!
这是因为 setTimeout 获取到了函数 user.sayHi,但它和对象分离开了。最后一行可以写为:
let f = user.sayHi;
setTimeout(f, 1000); // 用户上下文丢失
浏览器中的方法 setTimeout 有些特殊:它为函数的调用设定了 this=window(对于 Node.JS,this 则会成为时间对象,但其实 this 到底变成什么并不十分重要)。所以对于 this.firstName 它其实试图获取的是 window.firstName,这个变量并不存在。在其他一些类似的情况下,通常 this 就会成为 undefined。
这个需求很典型——我们希望将一个对象的方法传递到别的地方(这里——是为了调度程序)然后调用。如何确保它将会在正确的上下文中被调用呢?
解决方法:bind
原文中有两种方案,一种是使用一个包装函数,但在变量临时改变的时,仍会出问题。
所以此处仅展示第二种解决方案bind
函数对象提供了一个内建方法 bind,它可以固定住 this。
基本的语法是:
// 稍后将会有更复杂的语法
let boundFunc = func.bind(context);
func.bind(context) 的结果是一个特殊的像函数一样的“外来对象”,它可以像函数一样被调用并且透明的将调用传递给 func 并设置 this=context。
换句话说,调用 boundFunc 就像是调用 func 并且固定住了 this。
举个例子,这里 funcUser 将调用传递给了 func 同时 this=user:
let user = {
firstName: "John"
};
function func() {
console.log(this.firstName);
}
let funcUser = func.bind(user);
funcUser(); // John
这里 func.bind(user) 作为 func 的“边界变量”,同时固定了 this=user。
所有的参数都会被传递给初始的 func,就像本来就是调用了它一样,例如:
let user = {
firstName: "John"
};
function func(phrase) {
console.log(phrase + ', ' + this.firstName);
}
// 将 this 绑定给 user
let funcUser = func.bind(user);
funcUser("Hello"); // Hello, John(参数 "Hello" 被传递了,并且 this=user)
下面我们来尝试一个对象的方法:
let user = {
firstName: "John",
sayHi() {
console.log(`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) {
console.log(`${phrase}, ${this.firstName}!`);
}
};
let say = user.say.bind(user);
say("Hello"); // Hello, John ("Hello" 参数被传递给了函数 say)
say("Bye"); // Bye, John ("Bye" 被传递给了函数 say)