当将对象方法作为回调函数传递时,例如传递给 setTimeout
,有一个已知问题:“丢失 this
”。
在本章中,我们将了解解决此问题的方法。
丢失“this”
我们已经看到丢失 this
的示例。一旦方法在对象之外单独传递,this
就会丢失。
以下是可能发生在 setTimeout
的情况
let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
setTimeout(user.sayHi, 1000); // Hello, undefined!
正如我们所见,输出显示的不是 this.firstName
中的“John”,而是 undefined
!
这是因为 setTimeout
获取了函数 user.sayHi
,独立于对象。最后一行可以重写为
let f = user.sayHi;
setTimeout(f, 1000); // lost user context
浏览器中的 setTimeout
方法有点特殊:它为函数调用设置 this=window
(对于 Node.js,this
变为计时器对象,但此处并不重要)。因此,对于 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);
// ...the value of user changes within 1 second
user = {
sayHi() { alert("Another user in setTimeout!"); }
};
// Another user in setTimeout!
下一个解决方案保证不会发生这种情况。
解决方案 2:绑定
函数提供了一个内置方法 bind,允许修复 this
。
基本语法为
// more complex syntax will come a little later
let boundFunc = func.bind(context);
func.bind(context)
的结果是一个特殊的函数式“异域对象”,它可以作为函数调用,并透明地将调用传递给 func
,设置 this=context
。
换句话说,调用 boundFunc
就像具有固定 this
的 func
。
例如,此处 funcUser
使用 this=user
将调用传递给 func
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); // (*)
// can run it without an object
sayHi(); // Hello, John!
setTimeout(sayHi, 1000); // Hello, John!
// even if the value of user changes within 1 second
// sayHi uses the pre-bound value which is reference to the old user object
user = {
sayHi() { alert("Another user in setTimeout!"); }
};
在行 (*)
中,我们采用方法 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(object, methodNames)。
部分函数
到目前为止,我们只讨论了绑定 this
。让我们更进一步。
我们不仅可以绑定 this
,还可以绑定参数。这很少做,但有时会很方便。
bind
的完整语法
let bound = func.bind(context, [arg1], [arg2], ...);
它允许将上下文作为 this
绑定,并作为函数的起始参数。
例如,我们有一个乘法函数 mul(a, b)
function mul(a, b) {
return a * b;
}
让我们使用 bind
在其基础上创建一个函数 double
function mul(a, b) {
return a * b;
}
let double = mul.bind(null, 2);
alert( double(3) ); // = mul(2, 3) = 6
alert( double(4) ); // = mul(2, 4) = 8
alert( double(5) ); // = mul(2, 5) = 10
对 mul.bind(null, 2)
的调用创建了一个新函数 double
,该函数将调用传递给 mul
,将 null
固定为上下文,将 2
固定为第一个参数。其他参数“按原样”传递。
这称为 部分函数应用 - 我们通过固定现有函数的某些参数来创建一个新函数。
请注意,我们实际上没有在这里使用 this
。但 bind
需要它,所以我们必须放入类似 null
的东西。
下面代码中的函数 triple
将值乘以三
function mul(a, b) {
return a * b;
}
let triple = mul.bind(null, 3);
alert( triple(3) ); // = mul(3, 3) = 9
alert( triple(4) ); // = mul(3, 4) = 12
alert( triple(5) ); // = mul(3, 5) = 15
我们通常为什么要创建部分函数?
好处在于我们可以创建一个具有可读名称的独立函数(double
、triple
)。我们可以使用它,而不必每次都提供第一个参数,因为它是用 bind
固定下来的。
在其他情况下,当我们有一个非常通用的函数并希望为方便起见获得一个不太通用的变体时,部分应用很有用。
例如,我们有一个函数 send(from, to, text)
。然后,在 user
对象中,我们可能希望使用它的部分变体:sendTo(to, text)
,该变体从当前用户发送。
在没有上下文的情况下进行部分应用
如果我们想固定一些参数,但不想固定上下文 this
怎么办?例如,对于对象方法。
本机 bind
不允许这样做。我们不能仅仅省略上下文并跳转到参数。
幸运的是,可以轻松实现一个仅用于绑定参数的函数 partial
。
像这样
function partial(func, ...argsBound) {
return function(...args) { // (*)
return func.call(this, ...argsBound, ...args);
}
}
// Usage:
let user = {
firstName: "John",
say(time, phrase) {
alert(`[${time}] ${this.firstName}: ${phrase}!`);
}
};
// add a partial method with fixed time
user.sayNow = partial(user.say, new Date().getHours() + ':' + new Date().getMinutes());
user.sayNow("Hello");
// Something like:
// [10:00] John: Hello!
partial(func[, arg1, arg2...])
调用的结果是一个包装器 (*)
,它使用以下内容调用 func
- 与它获得的相同
this
(对于user.sayNow
调用,它为user
) - 然后为它提供
...argsBound
- 来自partial
调用的参数("10:00"
) - 然后为它提供
...args
- 传递给包装器的参数("Hello"
)
使用扩展语法很容易做到这一点,对吧?
此外,还有来自 lodash 库的准备好的 _.partial 实现。
摘要
方法 func.bind(context, ...args)
返回函数 func
的“绑定变体”,该变体修复了上下文 this
和给定的第一个参数(如果给定)。
我们通常应用 bind
来修复对象方法的 this
,以便我们可以将其传递到某个地方。例如,到 setTimeout
。
当我们修复现有函数的某些参数时,产生的(不太通用的)函数称为部分应用或部分。
当我们不想一遍又一遍地重复同一个参数时,部分应用很方便。比如,如果我们有一个 send(from, to)
函数,并且 from
对于我们的任务应该始终相同,我们可以得到一个部分并继续使用它。
评论
<code>
标记,对于多行代码,请将其包装在<pre>
标记中,对于 10 行以上的代码,请使用沙箱 (plnkr、jsbin、codepen…)