一、当将对象方法作为回调进行传递,例如传递给setTimeout,这儿会存在一个常见的问题:“丢失this”。下面我们来学习如何去解决这个问题。
丢失 “this”
一、JavaScript 中的函数具有动态的this。它取决于调用上下文。
丢失this
一、如果一个对象方法被传递到某处,或者在另一个上下文中被调用,则this将不再是对其对象的引用。
【示例1】此代码将显示undefined
class Button {
constructor(value) {
this.value = value;
}
click() {
alert(this.value);
}
}
let button = new Button("hello");
setTimeout(button.click, 1000); // undefined
this是如何丢失的
一、丢失this:一旦方法被传递到与对象分开的某个地方 ——this就丢失。
【示例1】使用setTimeout时this是如何丢失的:
let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
setTimeout(user.sayHi, 1000); // Hello, undefined!
1、正如我们所看到的,输出没有像this.firstName那样显示 “John”,而显示了undefined!
2、这是因为setTimeout获取到了函数user.sayHi,但它和对象分离开了。
3、最后一行可以被重写为:
let f = user.sayHi;
setTimeout(f, 1000); // 丢失了 user 上下文
4、浏览器中的setTimeout方法有些特殊:
(1)它为函数调用设定了this=window(对于 Node.js,this则会变为计时器(timer)对象,但在这儿并不重要)。
(2)所以对于this.firstName,它其实试图获取的是window.firstName,这个变量并不存在。
(3)在其他类似的情况下,通常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!
1、看起来不错,但是我们的代码结构中出现了一个小漏洞。
2、如果在setTimeout触发之前(有一秒的延迟!)user的值改变了怎么办?那么,突然间,它将调用错误的对象!
let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
setTimeout(() => user.sayHi(), 1000);
// user 的值在不到 1 秒的时间内发生了改变
user = {
sayHi() { alert("Another user in setTimeout!"); }
};
// Another user in setTimeout!
解决方案 2:bind / 将方法绑定到对象
一、函数提供了一个内建方法bind,它可以绑定this。
二、基本的语法是:
// 稍后将会有更复杂的语法
let boundFunc = func.bind(context);
1、func.bind(context)的结果是一个特殊的类似于函数的“外来对象(exotic object)”,它可以像函数一样被调用,并且透明地(transparently)将调用传递给func并设定this=context。
2、换句话说,boundFunc调用就像绑定了this的func。
【示例1】funcUser将调用传递给了func同时this=user:
let user = {
firstName: "John"
};
function func() {
alert(this.firstName);
}
let funcUser = func.bind(user);
funcUser(); // John
(1)这里的func.bind(user)作为func的“绑定的(bound)变体”,绑定了this=user。
(2)所有的参数(arguments)都被“原样”传递给了初始的func,例如:
let user = {
firstName: "John"
};
function func(phrase) {
alert(phrase + ', ' + this.firstName);
}
// 将 this 绑定到 user
let funcUser = func.bind(user);
funcUser("Hello"); // Hello, John(参数 "Hello" 被传递,并且 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 的值在不到 1 秒内发生了改变
// sayHi 还是会使用预先绑定(pre-bound)的值,该值是对旧的 user 对象的引用
user = {
sayHi() { alert("Another user in setTimeout!"); }
};
1、在(*)行,我们取了方法user.sayHi并将其绑定到user。sayHi是一个“绑定后(bound)”的方法,它可以被单独调用,也可以被传递给setTimeout—— 都没关系,函数上下文都会是正确的。
2、这里我们能够看到参数(arguments)都被“原样”传递了,只是this被bind绑定了:
let user = {
firstName: "John",
say(phrase) {
alert(`${phrase}, ${this.firstName}!`);
}
};
let say = user.say.bind(user);
say("Hello"); // Hello, John(参数 "Hello" 被传递给了 say)
say("Bye"); // Bye, John(参数 "Bye" 被传递给了 say)
四、如果一个对象有很多方法,并且我们都打算将它们都传递出去,那么我们可以在一个循环中完成所有方法的绑定:
for (let key in user) {
if (typeof user[key] == 'function') {
user[key] = user[key].bind(user);
}
}
1、JavaScript 库还提供了方便批量绑定的函数,例如 lodash 中的_.bindAll(object, methodNames)。
解决方案3: class / 使用类字段制作绑定方法
一、类字段提供了另一种非常优雅的语法:
class Button {
constructor(value) {
this.value = value;
}
click = () => {
alert(this.value);
}
}
let button = new Button("hello");
setTimeout(button.click, 1000); // hello
二、类字段click = () => {…}是基于每一个对象被创建的,在这里对于每一个Button对象都有一个独立的方法,在内部都有一个指向此对象的this。我们可以把button.click传递到任何地方,而且this的值总是正确的。
三、在浏览器环境中,它对于进行事件监听尤为有用。
参数绑定方案
一、当我们绑定一个现有的函数的某些参数时,绑定后的函数被称为partially applied或partial
偏函数(Partial functions)
一、到现在为止,我们只在谈论绑定this。让我们再深入一步。
二、我们不仅可以绑定this,还可以绑定参数(arguments)。虽然很少这么做,但有时它可以派上用场。
三、bind的完整语法如下:
let bound = func.bind(context, [arg1], [arg2], ...);
1、它允许将上下文绑定为this,以及绑定函数的起始参数。
【示例1】我们有一个乘法函数mul(a, b):
function mul(a, b) {
return a * b;
}
(1)让我们使用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
(2)对mul.bind(null, 2)的调用创建了一个新函数double,它将调用传递到mul,将null绑定为上下文,并将2绑定为第一个参数。并且,参数(arguments)均被“原样”传递。
① 这里我们实际上没有用到this。但是bind需要它,所以我们必须传入null之类的东西。
(3)它被称为偏函数应用程序(partial function application)—— 我们通过绑定先有函数的一些参数来创建一个新函数。
【示例2】下面这段代码中的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
四、为什么我们通常会创建一个偏函数?
1、好处是我们可以创建一个具有可读性高的名字(double,triple)的独立函数。我们可以使用它,并且不必每次都提供一个参数,因为参数是被绑定了的。
2、另一方面,当我们有一个非常通用的函数,并希望有一个通用型更低的该函数的变体时,偏函数会非常有用。
(1)例如,我们有一个函数send(from, to, text)。然后,在一个user对象的内部,我们可能希望对它使用send的偏函数变体:从当前 user 发送sendTo(to, text)。
没有上下文的情况
partial
一、当我们想绑定一些参数(arguments),但是这里没有上下文this,应该怎么办?例如,对于一个对象方法。
二、原生的bind不允许这种情况。我们不可以省略上下文直接跳到参数(arguments)。
三、幸运的是,仅绑定参数(arguments)的函数partial比较容易实现。
像这样:
function partial(func, ...argsBound) {
return function(...args) { // (*)
return func.call(this, ...argsBound, ...args);
}
}
// 用法:
let user = {
firstName: "John",
say(time, phrase) {
alert(`[${time}] ${this.firstName}: ${phrase}!`);
}
};
// 添加一个带有绑定时间的 partial 方法
user.sayNow = partial(user.say, new Date().getHours() + ':' + new Date().getMinutes());
user.sayNow("Hello");
// 类似于这样的一些内容:
// [10:00] John: Hello!
四、partial(func[, arg1, arg2…])调用的结果是一个包装器(*),它调用func并具有以下内容:
- 与它获得的函数具有相同的this(对于user.sayNow调用来说,它是user)
- 然后给它…argsBound—— 来自于partial调用的参数(”10:00”)
- 然后给它…args—— 给包装器的参数(”Hello”)
五、当我们不想一遍又一遍地重复相同的参数时,partial 非常有用。就像我们有一个send(from, to)函数,并且对于我们的任务来说,from应该总是一样的,那么我们就可以搞一个 partial 并使用它。
spread
lodash库
一、还有来自 lodash 库的现成的_.partial实现。