什么时候不使用箭头函数

作为一个前端渣渣,最近在写js代码的时候,开始使用一些ES6的新特性,结果到箭头函数部分“掉坑”了。网上大部分都说箭头函数怎么用,而很少说箭头什么时候 不要用,所以这里翻译一篇文章来说说什么时候不要用的问题。

原文地址,向原作者表示感谢。

看到代码每天都在进步是一件快乐的事情,在错误中学习,搜索更好的实现,创造新的特性,这些行为让代码在版本迭代中越来越好。

这就是JavaScript这几年发生的事,ES6带来了很多新特性:箭头函数、类等等。这些都是一些伟大的变化,特别是箭头函数。已经有很多的文章对箭头函数的特性进行了介绍,如果你刚刚接触ES6,我建议你去看这篇文章

然而事情都有两面性,新特性某些时候也会带来混乱,其中之一就是乱用箭头函数的问题。

这篇文章举了一些示例来说明什么地方你应该放弃使用箭头函数而去使用旧的函数声明表达式或者短方法语法(shorthand method syntax)。并且给出简短的解决方法,这可以让代码的可读性更高。

1.定义对象方法时(Defining methods on an object)

JavaScript中的方法就是一个作为对象属性的函数,当一个方法被调用时,this就指向了方法所属的对象。

1a.对象方法(Object literal)

因为箭头函数的语法简短,使用它来定义函数方法是很吸引人的。让我们试一下:

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可以参考廖大的网站,讲的很详细。)

所以调用this.arrow等同于调用window.arrow,即undefined

解决方法就是使用函数表达式或者短语法(ES6新特性)来定义方法,这种情况下this将由调用者决定,而不是闭包中。下面是修改后的版本:

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

1b.对象原型(Object prototype)

同样的规则也适用于prototype对象上,使用箭头函数来定义sayCatName方法,造成了错误的上下文环境为window

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  

使用老的函数表达式来解决:

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()这种方式调用时,我们将上下文环境绑定到了cat对象上。

2.回调函数中(Callback functions with dynamic context)

JavaScript中的this是一个很给力的特性,它准许在函数调用时改变上下文。调用目标对象时频繁的切换上下文,可以使代码看起来更加“自然”,就好象说“这个对象发生什么事情”一样。

然而箭头函数在声明时候静态的绑定了上下文环境,并且不可动态修改。这样的好处就是不用考虑this的词法作用域了。

很常见的一种情况就是给DOM元素添加监听事件。目标元素作为this触发了事件监听函数,这样可以简单的使用动态上下文环境了。

下面的例子尝试使用箭头函数作为回调函数:

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

在全局上下文中this被指向了window,当点击时间发生时,浏览器尝试在button的上下文中去调用回调函数,但箭头函数并没有改变之前定义的上下文。this.innerHTML等同于window.innerHTML并且没任何意义。

这时你不得不使用函数表达式,准许this根据目标元素来改变。

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

当用户点击按钮后,在回调函数中this指向了button,因此点击之后this.innerHTML = 'Clicked button'正确的修改了按钮文本。

3.构造函数中(Invoking constructors)

在构造函数中使用this来创建新对象,当执行new MyFunction()时,构造函数MyFunction的上下文是一个新的对象:this instanceof MyFunction === true

要注意,箭头函数并不能作为构造函数使用。JavaScript通过引发异常来隐式阻止那么做。不管怎么样,this在闭包的上下文中已经被设定而不是一个新创建的对象。换句话说,箭头函数作为构造函数使用并不是一种明确的做法并且会引起歧义。

来看看下面的代码会发生什么:

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

执行new Message('Hello World!')Message是一个箭头函数,JavaScript引发了一个TypeError来阻止Message作为构造函数来使用。

我认为ES6中明确的给出了详细的错误提示是非常好的,相反在之前的版本中会静默的失败而没有任何提示。

下面的例子使用函数表达式来正确的创建构造函数:

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

4.语句过短时(Too short syntax)

箭头函数可以省略括号、花括号甚至在函数体只有一句话时候连return都可以省略,这有助于写出简短的代码。

我的大学教授给学生一个很有意思的任务:使用C语言编写尽可能短的函数,这是一个好方法去学习、探索一个新的语言。

然而在真实世界中程序的代码会被很多开发者阅读。过于简洁的代码并不有助于你的同事正确理解函数的作用。

过于抽象的函数很难被理解,所以别手里有个锤子看什么都是钉子。让我们看下面这个栗子:

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

multiply返回两个数的乘积或者使用第一个参数为后续的运算创建一个闭包。

这个函数可以正常使用并且十分简短,但是第一眼看上去太难理解了。

为了可读性,我们将这个箭头函数还原回函数表达式:

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  

在简短和冗余间做出权衡让你的代码更加清晰明确。

5.结论(Conclusion)

毫无疑问箭头函数是一个不错的特性,正确的使用可以让代码更加简单明了,特别是早些时候不得不使用.bind()或者捕获上下文的时候。

事物都有两面性,你不能在需要动态上下文环境的情况下使用箭头函数:定义方法时,使用构造函数创建对象时,当处理事件时需要使用this从目标获取上下文的情况。


译者推荐链接

在我看箭头函数时一下链接对我帮助很大,值得一看:

  1. 廖雪峰的官方网站
  2. MDN箭头函数
  3. MDN函数表达式