翻译:道奇
作者:Dmitri Pavlutin
原文:How to Handle Easily 'this' in JavaScript
我喜欢JavaScript改变执行上下文的能力,也被称为this。
举个例子,可以在类数组的对象上使用数组方法:
const reduce = Array.prototype.reduce;
function sumArgs() {
return reduce.call(arguments, (sum, value) => {
return sum += value;
});
}
sumArgs(1, 2, 3); // => 6
另一方面,this关键字很难掌握。你可能经常在查找为什么this得到的值是不正确的。下面的章节会告诉你一些简单的方法,如何将this绑定到所需的值上。
在开始前,需要一个帮助函数execute(func),它以参数的形式执行函数:
function execute(func) {
return func();
}
execute(function() { return 10 }); // => 10
现在继续理解围绕this错误的本质:方法分离。
1.方法分离问题
Person类包含字段firstName和lastName,另外还有个返回person全名的getFullName()方法。
Person的一种实现可能是:
function Person(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
this.getFullName = function() {
this === agent; // => true
return `${this.firstName} ${this.lastName}`;
}
}
const agent = new Person('John', 'Smith');
agent.getFullName(); // => 'John Smith'
可以看到Person函数被当作构造函数调用:new Person('Jonh','Smith'),在Person函数内部,this是个新建的实例。
agent.getFullName返回person的全名:'John Smith'。如你所料,getFullName()方法内部的this等于agent。
如果agent.getFullName方法由帮助函数Execute执行会发生什么呢:
execute(agent.getFullName); // => 'undefined undefined'
执行结果会不对: 'undefined undefined',原因是this的值不正确。
在getFullName()内部this的值指向的是全局对象(在浏览器环境下就是window对象)。因为this等于window,求值表达式${window.firstName} ${window.lastName}的结果是'undefined undefined'。
这是因为当调用execute(agent.getFullName)函数时,函数与对象分离了。基本上就相当于只是一个普通的函数调用(而不是方法调用):
execute(agent.getFullName); // => 'undefined undefined'
//等于:
const getFullNameSeparated = agent.getFullName;
execute(getFullNameSeparated); // => 'undefined undefined'
这就是为什么我说 “函数与对象分离”。当方法分离后再执行,它就和它所在的源对象没有关联了。
如果要确保方法内的this指向准确的对象,就必须:
- 以属性访问的方式执行方法:
agent.getFullName() - 或者将
this静态的绑定到所在的对象(使用箭头函数,.bind()方法等等)
方法分离问题,导致this的值不对,会呈现多种不同的情况:
设置回调时
// `this` 在 `methodHandler()` 内部是全局对象
setTimeout(object.handlerMethod, 1000);
设置事件处理器时
// React: `this` 在 `methodHandler()`内部是全局对象
<button onClick={object.handlerMethod}>
Click me
</button>
尽管方法和对象分离了,还是有一些可以将this指向所需对象的好用的方法。
2. 关闭上下文
最简单的方式就是使用额外的变量self将this指向类的实例:
function Person(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
const self = this;
this.getFullName = function() {
self === agent; // => true
return `${self.firstName} ${self.lastName}`;
}
}
const agent = new Person('John', 'Smith');
agent.getFullName(); // => 'John Smith'
execute(agent.getFullName); // => 'John Smith'
getFullName()通过self变量,将方法手动绑定到this上。
现在调用execute(agent.getFullName)代码将正常执行,返回 'John Smith',因为getFullName()方法总是得到正确this值。
3. 箭头函数中this的词法
如果不通过额外的变量有没有方法静态的绑定this呢?是的,箭头函数就是做这个事情的。
使用箭头函数重构一下person:
function Person(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
this.getFullName = () => `${this.firstName} ${this.lastName}`;
}
const agent = new Person('John', 'Smith');
agent.getFullName(); // => 'John Smith'
execute(agent.getFullName); // => 'John Smith'
箭头函数只是从词法上绑定this,简单的说,它使用外部函数定义this的值。
当你要使用外部函数的上下文时,我推荐在所有的情况下都使用箭头函数。
4. 绑定上下文
现在我们再往前一步,使用ES2015的类来重构Person。
class Person {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
getFullName() {
return `${this.firstName} ${this.lastName}`;
}
}
const agent = new Person('John', 'Smith');
agent.getFullName(); // => 'John Smith'
execute(agent.getFullName); // => 'undefined undefined'
很不幸,即使使用最新的类语法,execute(agent.getFullName)还是返回'undefined undefined'。在类里,使用额外变量self或箭头函数来固定this的值是行不通的。
但是有个小技巧是使用bind()方法在构造函数里绑定方法的上下文:
class Person {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
this.getFullName = this.getFullName.bind(this);
}
getFullName() {
return `${this.firstName} ${this.lastName}`;
}
}
const agent = new Person('John', 'Smith');
agent.getFullName(); // => 'John Smith'
execute(agent.getFullName); // => 'John Smith'
构造函数内部this.getFullName = this.getFullName.bind(this)这行语句将getFullName()方法绑定到了类实例上。
execute(agent.getFullName) 按预期的那样执行,返回'John Smith'。
5. 胖箭头方法
上面的方法通过手动绑定上下文需要写一行样板代码(this.getFullName = this.getFullName.bind(this);)。幸运的是,还有优化的空间。
可以使用JavaScript的类字段的建议允许定义胖箭头方法:
class Person {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
getFullName = () => {
return `${this.firstName} ${this.lastName}`;
}
}
const agent = new Person('John', 'Smith');
agent.getFullName(); // => 'John Smith'
execute(agent.getFullName); // => 'John Smith'
就算将方法和它的对象分离,胖箭头方法getFullName = () => { ... }绑定还是会绑定到类实例上。这种是在类中绑定this最有效且最简洁的方式了。
6. 总结
方法和它的对象分离导致了很多关于this的误解,需要意识到这种影响。
为了静态绑定this,可以手动的使用额外的变量self关联正确的上下文对象。但是,更好的选择是使用箭头函数,它的语法上是天然设计绑定this的。
在类中,可以在构造函数中使用bind()方法手动地绑定类方法。
如果你不想写样板代码,新的JavaScript的类字段建议增加了胖箭头方法可以自动的将this绑定到类实例上。