函数绑定

53 阅读2分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第9天,点击查看活动详情

当将对象方法作为回调进行传递,例如传递给 setTimeout,这儿会存在一个常见的问题:“丢失 this”。

在本章中,我们会学习如何去解决这个问题。

丢失 “this”

我们已经看到了丢失 this 的例子。一旦方法被传递到与对象分开的某个地方 —— this 就丢失。

下面是使用 setTimeout 时 this 是如何丢失的:

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); // 丢失了 user 上下文

浏览器中的 setTimeout 方法有些特殊:它为函数调用设定了 this=window(对于 Node.js,this 则会变为计时器(timer)对象,但在这儿并不重要)。所以对于 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);

// ……user 的值在不到 1 秒的时间内发生了改变
user = {
  sayHi() { alert("Another user in setTimeout!"); }
};

// Another user in setTimeout!

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

解决方案 2:bind

函数提供了一个内建方法 bind,它可以绑定 this

基本的语法是:

// 稍后将会有更复杂的语法
let boundFunc = func.bind(context);

func.bind(context) 的结果是一个特殊的类似于函数的“外来对象(exotic object)”,它可以像函数一样被调用,并且透明地(transparently)将调用传递给 func 并设定 this=context

换句话说,boundFunc 调用就像绑定了 this 的 func

举个例子,这里的 funcUser 将调用传递给了 func 同时 this=user

let user = {
  firstName: "John"
};

function func() {
  alert(this.firstName);
}

let funcUser = func.bind(user);
funcUser(); // John

这里的 func.bind(user) 作为 func 的“绑定的(bound)变体”,绑定了 this=user

所有的参数(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!"); }
};

在 (*) 行,我们取了方法 user.sayHi 并将其绑定到 usersayHi 是一个“绑定后(bound)”的方法,它可以被单独调用,也可以被传递给 setTimeout —— 都没关系,函数上下文都会是正确的。

这里我们能够看到参数(arguments)都被“原样”传递了,只是 this 被 bind 绑定了:

let user = {
  firstName: "John",
  say(phrase) {
    alert(`${phrase}, ${this.firstName}!`);
  }
};

let say = user.say.bind(user);

say("Hello"); // Hello, John!(参数 "Hello" 被传递给了 saysay("Bye"); // Bye, John!(参数 "Bye" 被传递给了 say