stephen's blog

[object Object] object(s)
 

[译]什么时候“不要”用箭头函数

本文翻译于 https://rainsoft.io/when-not-to-use-arrow-functions-in-javascript/

看到你写的编程语言每天都在进化更新,是一件非常值得高兴的事情,通过从错误中学习、发现更好的解决办法以及创造出新的特性,来让这个过程不断的版本迭代。

近几年这些都发生在Javascrpit身上,尤其是ECMAScript6的出现给这门语言带来了更高的实用性: 箭头函数、类以及更多,这是非常棒的!

其中最有价值的特性之一便是箭头函数。同时有大量的文章来描述它的上下文透明性以及短语法。如果你是新接触ES6的话,可以从这篇文章来了解箭头函数。

但是水能载舟,也能覆舟。许多时候新特性会介绍的非常混乱,其中之一就是箭头函数被误用。

这篇文章通过情景引导你哪里需要绕过箭头函数,使用更合适的旧的函数表达式或新的短语法。同时对短语法有个心理准备,因为它会影响你代码的可读性。

1.在对象上定义方法

在javascript中,方法是储存在对象一个属性里的一个函数。当调用该方法的时候,this指向的是方法所属的对象。

1a.对象字面量

自从箭头函数有了短语法,使用它来定义方法变得更加吸引人,让我们尝试一下:

使用JS Bin尝试

1
2
3
4
5
6
7
8
9
10
var calculate = {
array: [1, 2, 3],
sum: () => {
console.log(this === window); // => true
return this.array.reduce((result, item) => result + item);
}
};
console.log(this === window); // => true
// Throws "TypeError: Cannot read property 'reduce' of undefined"
calculate.sum();

calculate.sum方法是使用箭头函数定义的,但是在调用calculate.sum的时候却抛出TypeError的错误,这是因为this.array的值是undefined.
当调用calculate对象上的sum()方法的时候,上下文依然是window,原因是箭头函数绑定了window对象的词法上下文。
执行this.array相当于执行window.array,它的值是undefined.

解决办法是使用函数表达式或者短语法(ECMAScript 6中可用)来定义方法。在这种情况下this指向的是调用对象,而不是邻近上下文。让我们来看看修改的版本:

使用JS Bin尝试

1
2
3
4
5
6
7
8
var calculate = {
array: [1, 2, 3],
sum() {
console.log(this === calculate); // => true
return this.array.reduce((result, item) => result + item);
}
};
calculate.sum(); // => 6

因为sum是一个普通函数,调用calculate.sum()其中的this指向的是calculate对象,this.array便是array的一个引用,所以元素之和计算正确: 6 。

1b.对象原型

相同的规则也适用于通过原型对象来定义方法。如下面例子,如果使用箭头函数来定义sayCatName方法,会指向一个不正确的上下文window:

使用JS Bin尝试

1
2
3
4
5
6
7
8
9
function MyCat(name) {
this.catName = name;
}
MyCat.prototype.sayCatName = () => {
console.log(this === window); // => true
return this.catName;
};
var cat = new MyCat('Mew');
cat.sayCatName(); // => undefined

使用保守派的函数表达式:

使用JS Bin尝试

1
2
3
4
5
6
7
8
9
function MyCat(name) {
this.catName = name;
}
MyCat.prototype.sayCatName = function() {
console.log(this === cat); // => true
return this.catName;
};
var cat = new MyCat('Mew');
cat.sayCatName(); // => 'Mew'

当你调用cat.sayCatName()方法的时候,sayCatName这个普通函数将上下文改变成了cat对象。

2.结合动态上下文的回调函数

this是javascript中一个非常强大的特性。它允许通过函数调用的方式来改变上下文。通常来说,使上下文为函数调用时候的目标对象,会让代码更加自然化,也就是说“让事情发生在对象上”。

然而,箭头函数在声明的时候是静态的绑定上下文,不可能是动态的。这种情况下this是不必要的。

在客户端编程中,给DOM元素绑定监听事件是一个十分常见的事情,一个事件以this作为目标元素去触发事件处理函数,是动态上下文最简便的用法。

下面的例子是使用箭头函数来作为事件处理函数:

使用JS Bin尝试

1
2
3
4
5
var button = document.getElementById('myButton');
button.addEventListener('click', () => {
console.log(this === window); // => true
this.innerHTML = 'Clicked button';
});

上面的例子中,箭头函数中的this指向的是window,也就是被定义在了全局的上下文。当一个点击事件发生时,浏览器尝试去调用button上下文中的事件处理函数,但是此时箭头函数并没有改变它之前定义的上下文(译者注:也就是window)。

所以,this.innerHTML的值和window.innerHTML的相等,这是没意义的。

你不得不使用函数表达式,来允许你改变this所指向的上下文。

使用JS Bin尝试

1
2
3
4
5
var button = document.getElementById('myButton');
button.addEventListener('click', function() {
console.log(this === button); // => true
this.innerHTML = 'Clicked button';
});

当用户点击按钮,事件处理函数中的this指向的是button。所以this.innnerHTML = 'Click Button'正确的的修改了按钮的文本,并且反映了点击的状态。

3. 调用构造器

this在构造调用中会指向一个新创建的对象。当执行new MyFunction()的时候,构造函数MyFunction的上下文是一个新的对象: this instanceof MyFunction === true.
注意到箭头函数不能用在构造器上,Javascript会通过抛出异常来隐式的预防这个。
无论如何,this会指向邻近的上下文而不是新创建的对象。换句话说,一个箭头函数的构造器并没有什么意义,相反可能会造成歧义。
让我们来看看如果使用箭头函数构造器:

使用JS Bin尝试

1
2
3
4
5
var Message = (text) => {
this.text = text;
};
// Throws "TypeError: Message is not a constructor"
var helloMessage = new Message('Hello World!');

Message是一个箭头函数,如果执行new Message('Hello World!') ,Javascript会抛出一个TypeError,也就是说Message不能当做构造器。
我认为相对于之前javascript版本的静默失败,ECMAScript 6 在这种情况下提供提供包含错误信息的失败提示会是更佳高效的实践。

上面的例子可以通过一个函数表达式来修正,这是一种正确的方式(包括函数声明)来创建构造器:

使用JS Bin尝试

1
2
3
4
5
var Message = function(text) {
this.text = text;
};
var helloMessage = new Message('Hello World!');
console.log(helloMessage.text); // => 'Hello World!'

4.最短语法

箭头函数有个非常棒的属性,即可以省略参数的括号,如果函数体只有一条语句,可以省略代码块的花括号{}以及return。这可以帮助你写更短的函数。

曾经我的大学编程老师给学生布置了一个非常有趣的任务:使用C语言通过一个最短的函数来计算字符串的长度。这是学习和探索新语言一个非常好的途径。

尽管如此在现实世界中你的应用代码会被许多开发者阅读。最短语法不是一直都适合给你的同事快速理解函数的意思。

某种程度上来说,压缩函数会让阅读变得更为困难,所以别在这里投入太大的热情。让我们来看看这个例子:

使用JS Bin尝试

1
2
3
4
let multiply = (a, b) => b === undefined ? b => a * b : a * b;
let double = multiply(2);
double(3); // => 6
multiply(2, 3); // => 6

multiply返回了两个数字相乘的结果或者说为了接下来的运算,关联了第一个参数的闭包。
函数运行良好并且看起来非常短,但是它可能在第一眼看的时候比较难的理解。

为了让它的可读性更高,可以给箭头函数添加可选的花括号和return语句或者用一些普通函数:

使用JS Bin尝试

1
2
3
4
5
6
7
8
9
10
11
function multiply(a, b) {
if (b === undefined) {
return function(b) {
return a * b;
}
}
return a * b;
}
let double = multiply(2);
double(3); // => 6
multiply(2, 3); // => 6

最好的方式就是在短和长之间寻找一个平衡点来让你Javascript代码更为简单。

5.结论

毫无疑问,箭头函数是一个非常棒的增强特性。如果使用正确的话,它能在很多地方带来便利,比如早期你不得不使用 .bind() 或者试图去捕获上下文。同时它也让代码变得更加轻便。

在一些情况下,优势和劣势是并存的。当要求动态上下文的时候,你就不能使用箭头函数,比如定义方法,用构造器创建对象,当要处理事件时用 this 获取目标。

一些参考文章

Gentle explanation of ‘this’ keyword in JavaScript
JavaScript variables hoisting in details
The legend of JavaScript equality operator