箭头函数

我们在本章早先接触了函数中this绑定的复杂性,而且在本系列的 this与对象原型 中也以相当的篇幅讲解过。理解普通函数中基于this的编程带来的挫折是很重要的,因为这是ES6的新=>箭头函数的主要动机。

作为与普通函数的比较,我们首先来展示一下箭头函数看起来什么样:

  1. function foo(x,y) {
  2. return x + y;
  3. }
  4. // 对比
  5. var foo = (x,y) => x + y;

箭头函数的定义由一个参数列表(零个或多个参数,如果参数不是只有一个,需要有一个( .. )包围这些参数)组成,紧跟着是一个=>符号,然后是一个函数体。

所以,在前面的代码段中,箭头函数只是(x,y) => x + y这一部分,而这个函数的引用刚好被赋值给了变量foo

函数体仅在含有多于一个表达式,或者由一个非表达式语句组成时才需要用{ .. }括起来。如果仅含有一个表达式,而且你省略了外围的{ .. },那么在这个表达式前面就会有一个隐含的return,就像前面的代码段中展示的那样。

这里是一些其他种类的箭头函数:

  1. var f1 = () => 12;
  2. var f2 = x => x * 2;
  3. var f3 = (x,y) => {
  4. var z = x * 2 + y;
  5. y++;
  6. x *= 3;
  7. return (x + y + z) / 2;
  8. };

箭头函数 总是 函数表达式;不存在箭头函数声明。而且很明显它们都是匿名函数表达式 —— 它们没有可以用于递归或者事件绑定/解除的命名引用 —— 但在第七章的“函数名”中将会讲解为了调试的目的而存在的ES6函数名接口规则。

注意: 普通函数参数的所有功能对于箭头函数都是可用的,包括默认值,解构,剩余参数,等等。

箭头函数拥有漂亮,简短的语法,这使得它们在表面上看起来对于编写简洁代码很有吸引力。确实,几乎所有关于ES6的文献(除了这个系列中的书目)看起来都立即将箭头函数仅仅认作“新函数”。

这说明在关于箭头函数的讨论中,几乎所有的例子都是简短的单语句工具,比如那些作为回调传递给各种工具的箭头函数。例如:

  1. var a = [1,2,3,4,5];
  2. a = a.map( v => v * 2 );
  3. console.log( a ); // [2,4,6,8,10]

在这些情况下,你的内联函数表达式很适合这种在一个单独语句中快速计算并返回结果的模式,对于更繁冗的function关键字和语法来说箭头函数确实看起来是一个很吸人,而且轻量的替代品。

大多数人看着这样简洁的例子都倾向于发出“哦……!啊……!”的感叹,就像我想象中你刚刚做的那样!

然而我要警示你的是,在我看来,使用箭头函数的语法代替普通的,多语句函数,特别是那些可以被自然地表达为函数声明的函数,是某种误用。

回忆本章早前的字符串字面量标签函数dollabillsyall(..) —— 让我们将它改为使用=>语法:

  1. var dollabillsyall = (strings, ...values) =>
  2. strings.reduce( (s,v,idx) => {
  3. if (idx > 0) {
  4. if (typeof values[idx-1] == "number") {
  5. // look, also using interpolated
  6. // string literals!
  7. s += `$${values[idx-1].toFixed( 2 )}`;
  8. }
  9. else {
  10. s += values[idx-1];
  11. }
  12. }
  13. return s + v;
  14. }, "" );

在这个例子中,我做的唯一修改是删除了functionreturn,和一些{ .. },然后插入了=>和一个var。这是对代码可读性的重大改进吗?呵呵。

实际上我会争论,缺少return和外部的{ .. }在某种程度上模糊了这样的事实:reduce(..)调用是函数dollabillsyall(..)中唯一的语句,而且它的结果是这个调用的预期结果。另外,那些受过训练而习惯于在代码中搜索function关键字来寻找作用域边界的眼睛,现在需要搜索=>标志,在密集的代码中这绝对会更加困难。

虽然不是一个硬性规则,但是我要说从=>箭头函数转换得来的可读性,与被转换的函数长度成反比。函数越长,=>能帮的忙越少;函数越短,=>的闪光之处就越多。

我觉得这样做更明智也更合理:在你需要短的内联函数表达式的地方采用=>,但保持你的一般长度的主函数原封不动。

不只是简短的语法,而是this

曾经集中在=>上的大多数注意力都是它通过在你的代码中除去functionreturn,和{ .. }来节省那些宝贵的击键。

但是至此我们一直忽略了一个重要的细节。我在这一节最开始的时候说过,=>函数与this绑定行为密切相关。事实上,=>箭头函数 主要的设计目的 就是以一种特定的方式改变this的行为,解决在this敏感的编码中的一个痛点。

节省击键是掩人耳目的东西,至多是一个误导人的配角。

让我们重温本章早前的另一个例子:

  1. var controller = {
  2. makeRequest: function(..){
  3. var self = this;
  4. btn.addEventListener( "click", function(){
  5. // ..
  6. self.makeRequest(..);
  7. }, false );
  8. }
  9. };

我们使用了黑科技var self = this,然后引用了self.makeRequest(..),因为在我们传递给addEventListener(..)的回调函数内部,this绑定将与makeRequest(..)本身中的this绑定不同。换句话说,因为this绑定是动态的,我们通过self变量退回到了可预测的词法作用域。

在这其中我们终于可以看到=>箭头函数主要的设计特性了。在箭头函数内部,this绑定不是动态的,而是词法的。在前一个代码段中,如果我们在回调里使用一个箭头函数,this将会不出所料地成为我们希望它成为的东西。

考虑如下代码:

  1. var controller = {
  2. makeRequest: function(..){
  3. btn.addEventListener( "click", () => {
  4. // ..
  5. this.makeRequest(..);
  6. }, false );
  7. }
  8. };

前面代码段的箭头函数中的词法this现在指向的值与外围的makeRequest(..)函数相同。换句话说,=>var self = this的语法上的替代品。

var self = this(或者,另一种选择是,.bind(this)调用)通常可以帮忙的情况下,=>箭头函数是一个基于相同原则的很好的替代操作。听起来很棒,是吧?

没那么简单。

如果=>取代var self = this.bind(this)可以工作,那么猜猜=>用于一个 不需要 var self = this就能工作的this敏感的函数会发生么?你可能会猜到它将会把事情搞砸。没错。

考虑如下代码:

  1. var controller = {
  2. makeRequest: (..) => {
  3. // ..
  4. this.helper(..);
  5. },
  6. helper: (..) => {
  7. // ..
  8. }
  9. };
  10. controller.makeRequest(..);

虽然我们以controller.makeRequest(..)的方式进行了调用,但是this.helper引用失败了,因为这里的this没有像平常那样指向controller。那么它指向哪里?它通过词法继承了外围的作用域中的this。在前面的代码段中,它是全局作用域,this指向了全局作用域。呃。

除了词法的this以外,箭头函数还拥有词法的arguments —— 它们没有自己的arguments数组,而是从它们的上层继承下来 —— 同样还有词法的supernew.target(参见第三章的“类”)。

所以,关于=>在什么情况下合适或不合适,我们现在可以推论出一组更加微妙的规则:

  • 如果你有一个简短的,单语句内联函数表达式,它唯一的语句是某个计算后的值的return语句,并且 这个函数没有在它内部制造一个this引用,并且 没有自引用(递归,事件绑定/解除),并且 你合理地预期这个函数绝不会变得需要this引用或自引用,那么你就可能安全地将它重构为一个=>箭头函数。
  • 如果你有一个内部函数表达式,它依赖于外围函数的var self = this黑科技或者.bind(this)调用来确保正确的this绑定,那么这个内部函数表达式就可能安全地变为一个=>箭头函数。
  • 如果你有一个内部函数表达式,它依赖于外围函数的类似于var args = Array.prototype.slice.call(arguments)这样的东西来制造一个arguments的词法拷贝,那么这个内部函数就可能安全地变为一个=>箭头函数。
  • 对于其他的所有东西 —— 普通函数声明,较长的多语句函数表达式,需要词法名称标识符进行自引用(递归等)的函数,和任何其他不符合前述性质的函数 —— 你就可能应当避免=>函数语法。

底线:=>thisarguments,和super的词法绑定有关。它们是ES6为了修正一些常见的问题而被有意设计的特性,而不是为了修正bug,怪异的代码,或者错误。

不要相信任何说=>主要是,或者几乎是,为了减少几下击键的炒作。无论你是省下还是浪费了这几下击键,你都应当确切地知道你打入的每个字母是为了做什么。

提示: 如果你有一个函数,由于上述各种清楚的原因而不适合成为一个=>箭头函数,但同时它又被声明为一个对象字面量的一部分,那么回想一下本章早先的“简约方法”,它有简短函数语法的另一种选择。

对于如何/为何选用一个箭头函数,如果你喜欢一个可视化的决策图的话:

箭头函数 - 图1