在这篇博文中,我将采取了一种不同的方式来解释 JavaScript 中的 this :我假设箭头函数是真正的函数,而普通函数是特殊结构的方法。我认为这样更容易理解 this – 试试看。
注:在没特殊说明的情况下,示例默认在 strict mode(严格模式) 下运行,即加上 'use strict'
。
1.两种类型的函数
在这篇文章中,我们关注两种不同类型的函数:
- 普通函数:
function () {}
- 箭头函数:
() => {}
1.1 普通函数
如下创建一个普通函数v:
function add(x, y) { return x + y; }
每个普通函数都有隐含的参数 this
,这个参数在被调用时总是被自动填充。换句话说,以下两个表达式是等价的(在严格模式下)。
add(3, 5) add.call(undefined, 3, 5);
如果你嵌套普通的函数,this
会被覆盖:
function outer() { function inner() { console.log(this); // undefined } console.log(this); // 'outer' inner(); } outer.call('outer');
在 inner()
里面, this
并不是指向 outer()
中的 this
,因为 inner()
里有它自己的 this
。这与变量在嵌套作用域中的工作方式类似:
const _this = 'outer'; console.log(_this); // 'outer' { const _this = undefined; console.log(_this); // undefined }
由于普通函数总是有隐含的参数 this
,所以对于普通函数来说更好的名字就是“方法”。
1.2 箭头函数
如下方式创建一个箭头函数(我正在使用 block(块) 语法 ,使其看起来有点类似于函数定义):
const add = (x, y) => { return x + y; };
如果你在一个普通函数里面嵌套一个箭头函数,那么 this
函数不会被覆盖:
function outer() { const inner = () => { console.log(this); // 'outer' }; console.log(this); // 'outer' inner(); } outer.call('outer');
由于箭头功能的行为,我也偶尔称他们为“真正函数”。它们与大多数编程语言中的函数类似 – 比普通函数更重要。
请注意,箭头函数中的 this
不受 .call()
,或者其他任何方式的影响。箭头函数中的 this
总是由箭头函数创建时的作用域决定。例如:
function ordinary() { const arrow = () => this; console.log(arrow.call('goodbye')); // 'hello' } ordinary.call('hello');
1.3 作为方法的普通函数
如果一个普通函数是一个对象的属性值,它就成为一个方法:
愚人码头注:一个普通函数总是作为一个方法被调用,如果它不被作为对象的属性调用,它将在全局对象的上下文中被隐式地调用(即使它不作为全局对象的属性存在)。
const obj = { prop: function () {} };
访问对象属性的一种方法是通过点操作符(.
)。该操作符有两种不同的模式:
- 获取和设置属性:
obj.prop
- 调用方法:
obj.prop(x, y)
后者相当于:
obj.prop.call(obj, x, y)
你可以再次看到,当调用普通函数时, this
总是被填充。
JavaScript 为定义方法提供了特别的,方便的语法:
const obj = { prop() {} };
2.常见的陷阱
通过我们刚刚学到的东西,让我们来看看常见的陷阱。
2.1 陷阱:在 Promises 回调中访问 this
一旦异步函数 cleanupAsync()
完成,请考虑以下基于Promise的代码,在该代码中我们打印 “Done”。
// 在类或对象字面量中: performCleanup() { cleanupAsync() .then(function () { this.logStatus('Done'); // (A) }); }
问题是(A)行中的 this.logStatus()
调用失败,因为 this
不是指向 .performCleanup()
的那个 this
– 也就是回调中的 this
被覆盖。 换句话说:我们应该使用一个箭头函数,而不是一个普通函数。如果我们这样做,一切运作良好:
// 在类或对象字面量中: performCleanup() { cleanupAsync() .then(() => { this.logStatus('Done'); }); }
2.2 陷阱:在 .map() 回调中访问 this
同样,以下代码在(A)行中失败,因为回调会覆盖方法 .prefixNames()
的 this
。
// 在类或对象字面量中: prefixNames(names) { return names.map(function (name) { return this.company + ': ' + name; // (A) }); }
再次,我们可以通过使用箭头函数来修复它:
// 在类或对象字面量中: prefixNames(names) { return names.map( name => this.company + ': ' + name); }
2.3 陷阱:使用方法作为回调
以下是用于UI组件的类。
class UiComponent { constructor(name) { this.name = name; const button = document.getElementById('myButton'); button.addEventListener('click', this.handleClick); // (A) } handleClick() { console.log('Clicked '+this.name); // (B) } }
在(A)行中,UiComponent
为点击注册一个事件处理程序。唉,如果这个处理程序被触发,你会得到一个错误:
TypeError: Cannot read property 'name' of undefined
为什么呢? 在(A)行中,我们使用了普通的点操作符,而不是特殊的方法调用点操作符。因此,存储在 handleClick
中的函数成为处理程序。也就是说,大致发生以下事情。
const handler = this.handleClick; handler(); // 等同于: handler.call(undefined);
结果,this.name
在(B)行中失败了。
那么我们如何解决这个问题呢?问题在于调用方法的点操作符不是简单的读取属性的组合,然后调用结果。它做更多。所以,当我们提取一个方法的时候,我们需要亲自提供缺失的部分,并在(A)行中,通过函数的.bind()
方法为 this
填充一个固定值:
class UiComponent { constructor(name) { this.name = name; const button = document.getElementById('myButton'); button.addEventListener( 'click', this.handleClick.bind(this)); // (A) } handleClick() { console.log('Clicked '+this.name); } }
现在, this
是固定的,并且不会通过正常的函数调用进行更改。
function returnThis() { return this; } const bound = returnThis.bind('hello'); bound(); // 'hello' bound.call(undefined); // 'hello'
3.保持安全的规则
避免 this
问题的最简单方法是避免使用普通函数,并且总是使用方法定义或箭头函数。
不过,我喜欢声明式函数的语法。提升有时也很有用。如果你不在里面引用 this
,你可以安全地使用它们。有一个 ESLint 规则可以帮你解决这个问题。
3.1 不要把 this 当成一个参数
一些API通过 this
提供类似参数的信息。我不喜欢这样,因为它阻止了你使用箭头函数,并且违背了最初提到的简单的经验法则。
我们来看一个例子:beforeEach()
函数通过 this
将一个 API 对象传递给它的回调函数。
beforeEach(function () { this.addMatchers({ // 访问 API 对象 toBeInRange: function (start, end) { ··· } }); });
重写这个函数很容易:
beforeEach(api => { api.addMatchers({ toBeInRange(start, end) { ··· } }); });
4.扩展阅读
- 全面理解 JavaScript 中的 this
- 博客文章“JavaScript 中的 this :工作原理和陷阱 [深入研究 this ]
- “探索ES6”中的 Callable entities in ECMAScript 6” 章节