原作者:江凌
箭头一族缺少的家庭成员
在JavaScript出现以来,箭头(Arrow)就一直是其语法的一部分。一般来说,JavaScript教程的第一篇就会讲如何在HTML中插入箭头括号来作为注释,这会阻止不支持JS的浏览器错误的将你的JS代码作为文字展现出来,比如这个:
<script language="javascript">
<!--
document.bgColor = "brown"; // red
// -->
</script>
老旧的浏览器只会看到两个不支持的tag以及一个注释,而现代浏览器则会看到JS代码。
注:其实这是一种hack写法,在如今现代浏览器都支持JS的情况下已经慢慢不会再出现这种代码了
为了支持这个这个古怪的hack写法,浏览器的JavaScript引擎把 <!--
当成了一行注释,这不是开玩笑,这成为了这个语言写法的一部分,并且直到现在都能正常使用。不只是在HTML的<script>
中,在JS文件中也是一样,甚至在Node中它也是行得通的。
很巧合的是,我们在ES6中也首次有了注释的标准格式但是这不是我们现在想要说的箭头了,在此不赘述。
在-->之后的也会被当成一个注释,这点很古怪。你可以尝试在node里输入-->console.log(1)
结果发现被作为一个注释而没有被执行。
但是更古怪的是,只有在箭头出现在行首的时候才会被当成注释,这是因为-->在JS中是一个操作符,是一个趋向操作符!
function countdown(n) {
while (n --> 0) // "n goes to zero"
alert(n);
blastoff();
}
这个代码可以执行。它会从n一直循环到0,这不是ES6的特性,只是有一点小误导的简单特性,你能理解发生了什么吗?谜题的答案在这里可以找到。
那么,本该四人组的箭头还缺了谁呢?
<!-- 单行注释
--> 趋向操作符
<= 小于等于
=> ?
=> 是ES6中新的用法,也就是本节要讲述的内容
更加简短的函数声明
JavaScript的一个很有趣的特性就是在你需要一个函数的时候,你可以把它写到文件中的任何一个地方。
举个例子,想象当你想要告诉浏览器你点击了一个特定的按钮的时候,你只需要打出:
$("#confetti-btn").click(
jQuery的click方法只有一个参数:一个函数,没有问题,你甚至可以直接这样写:
$("#confetti-btn").click(function (event) {
playTrumpet();
fireConfettiCannon();
});
在JS开始盛行前,这样的代码是令人奇怪的,因为很多语言当时都没有类似的写法。除了1958年List语言的lambda(匿名)函数有函数表达式功能,C++、Python、C#等语言长时间里都是没有的。到了现在,lambda已经随处可见了,这多亏了JS的功劳。
//六种语言的函数
function (a) { return a > 0; } // JS
[](int a) { return a > 0; } // C++
(lambda (a) (> a 0)) ;; Lisp
lambda a: a > 0 # Python
a => a > 0 // C#
a -> a > 0 // Java
新的箭头语法
ES6为我们介绍了一个新的可以用来写函数的语法
// ES5
var selected = allJobs.filter(function (job) {
return job.isSelected();
});
// ES6
var selected = allJobs.filter(job => job.isSelected());
当你只是需要一个简单的单变量函数,箭头函数写起来就非常简单 Identifier => Expression 你可以省去了写function和return了,当然,还有中括号,小括号和分号。
(个人来说,我真的很喜欢这个语法,因为我现在不需要再每次都书写function了,我经常错写成functoin,而且每次都要回来检查和改正它)
如果你想要写一个多参数的函数(或者没有参数亦或是Rest参数,甚至是一个解构的变量),都是非常简单的,你可以直接把他们用括号包起来。
// ES5
var total = values.reduce(function (a, b) {
return a + b;
}, 0);
// ES6
var total = values.reduce((a, b) => a + b, 0);
我认为这是最简洁的书写格式。
箭头函数的功用与Underscore.js和immutable等库的功能类似,immutable的示例文档中全部都是使用ES6来编写的,因此使用了大量的箭头函数。
除了函数样式编写,箭头函数还可以包含区块语句而不仅仅是单一表达式。例如:
// ES5
$("#confetti-btn").click(function (event) {
playTrumpet();
fireConfettiCannon();
});
Here’s how it will look in ES6:
// ES6
$("#confetti-btn").click(event => {
playTrumpet();
fireConfettiCannon();
});
只有一点点的改善,还不如使用Promise的提升更多。
但是有需要注意的几点:
1.当箭头函数包含的是区块的时候,它就不会自动return我们的结果
2.而且当我们想要直接使用箭头函数创造空对象,一定要将其包裹在括号里
var chewToys = puppies.map(puppy => {}); // BUG!
var chewToys = puppies.map(puppy => ({})); // ok
因为空对象和空区块长的一模一样,ES6在编译到{
的时候会直接认为它是一个区块,而不是一个Object的开始,所以上面出bug的写法什么都不会做,因为其函数内部只是返回了一堆undefined
。
虽然我们会有很多疑惑,比如 {key:value} 这种也会看起来像是一个有标签陈述式的区块,而不是一个对象,不过幸运的是{
是唯一的模糊字,所以我们只需要记住我们的对象要用括号包裹住就好。
只能继承parent的this值
普通函数与箭头函数有个微小的不同点。箭头函数没有自己的this值,其this值是通过继承其它传入对象而获得的。
JS是如何处理this的呢?this的值又是从何而来的呢?这可不是个简单的问题。如果你觉得这个已经很简单了,那么你真的是一名大牛了。
其中不论函数有没有真的需要处理this,函数都是会接收到的。你写过如下代码吗?
{//ES5
...
addAll: function addAll(pieces) {
var self = this;
_.each(pieces, function (piece) {
self.add(piece);
});
},
...
}
其实你想写的内联函数仅仅是this.add(piece)。然而内联函数不会继承外部函数的this值。在内联函数中,this的值是window或undefined。临时变量self用于向内联函数传入外部this值。
在ES6中,如果遵循如下原则则可避免类似的做法:
- 使用非箭头函数处理由object.method()语法调用的方法。因为它们会接收到来自调用者的有意义的this值.
- 在其它场合都使用箭头函数.
// ES6
{
...
addAll: function addAll(pieces) {
_.each(pieces, piece => this.add(piece));
},
...
}
在ES6的版本中,你会注意到addAll函数已经从其调用者获得了this的值,而且它的内联函数是一个箭头函数,所以内敛函数的this继承了addAll的this。
在Object中其实ES6可以更为精简的写方法了,这是因为ES6中的增强字面量(enhanced object literals).
{ //ES6 with method syntax
...
addAll(pieces) {
_.each(pieces, piece => this.add(piece));
},
...
}
箭头函数规范
箭头函数(Arrow Function)并非全能,在一些特别的场景并不适用,比如其没有函数名字,无法使用递归
- 所有箭头函数的参数均使用()包裹,即便只有一个参数
// Good Style
let foo = (x) => x+1;
// Bad Style
let foo = x => x+1;
- 定义函数尽可能使用箭头函数
// Good Style
let foo = () => {};
// Bad Style
function foo() {}
// Bad Style
let foo = function () {}
当然如上文的那些特殊情况不适用箭头函数的则不要使用
- 在对象或者Class中定义函数,使用箭头函数
这样能够最大程度的减少我们写一些多余的代码并且增加了代码的可读性。
拓展阅读
- MDN 箭头函数规范
- 本文由 ES6 In Depth: Arrow functions 翻译润色重新组织内容后合成