深入浅出ES6,深入理解JavaScript中的箭头函数

只有支持 JavaScript,而在JS中–&gt

从一开始箭头就是 JavaScript 的一部分,在第一个 JavaScript
中就建议将内联的脚本代码包裹在 HTML 的注释中,这可以防止那些不支持
JavaScript
的浏览器错误滴将你的代码显示为明文。你也许写过下面这样的代码:

箭头符号在JavaScript诞生时就已经存在,当初第一个JavaScript教程曾建议在HTML注释内包裹行内脚本,这样可以避免不支持JS的浏览器误将JS代码显示为文本。你会写这样的代码:

<script language="javascript">
<!--
  document.bgColor = "brown"; // red
// -->
</script>

<script language="javascript">
<!--
  document.bgColor = "brown"; // red
// -->
</script>

<scriptlanguage=”javascript”>

古老的浏览器将看到两个不被支持的标签和一段注释,只有支持 JavaScript
的新浏览器才会将其解析为 JavaScript 代码。

<!–

为了支持这个古怪的特性,浏览器的 JavaScript 引擎把 <!–
作为一个单行注释的开始,这不是开玩笑的,这一直都是这门语言的一部分,并且至今还能用,不仅仅在
<script> 标签内的首行,而是在 JavaScript
代码的任何部位都可用,它甚至还能在 Node 中使用。

   document.bgColor = “brown”;  // red

凑巧的是,这种风格的注释在 ES6
中首次被标准化。但这并不是我们将谈论的箭头。

   // –>

–> 也表示一个单行注释,与 HTML 不同的是,在 HTML 中,–>
之前的部分是注释内容,而在 JavaScript 中,在 –> 之后的行才是注释。

</script>

只有当 –>
出现在一行的开始时,才表示该箭头是一个注释,因为在其他情况下,–>
是一个操作符(goes to)。

老式浏览器会将这段代码解析为两个不支持的标签和一条注释,只有新式浏览器才能识别出其中的JS代码。

function countdown(n) {
 while (n-->0) // "n goes to zero"
  alert(n);
 blastoff();
}

function countdown(n) {
 while (n-->0) // "n goes to zero"
  alert(n);
 blastoff();
}

为了支持这种奇怪的hack方式,浏览器中的JavaScript引擎将标签后的首行,在JS代码的每个角落你都有可能见到它,甚至在Node中也是如此。–>

上面代码是真实能运行的。循环运行直到 n 为 0,这并不是 ES6
的新特性,但结合我们熟悉的特性,这具有很强的误导性。你能搞明白上面代码的运行情况吗?你可以在
Stack Overflow 上找到相应的解答。

碰巧,这种注释风格首次在ES6中被标准化了,但在新标准中箭头被用来做其它事情。

当然还有一个箭头,那就是小于等于操作符
<=,也许你还可以找到使用箭头的地方,但我们还是停下来,看一个我们从没见过的箭头:

箭头序列–>同样是单行注释的一部分。古怪的是,在HTML中–>之前的字符是注释的一部分,而在JS中–>之后的部分才是注释。

  •     <!– 单行注释
  •     –> goes to 操作符
  •     <= 小于等于操作符
  •     => ???

你一定感到陌生的是,只有当箭头在行首时才会注释当前行。这是因为在其它上下文中,–>是一个JS运算符:“趋向于”运算符!

那么,=> 表示什么呢?这就是本文将讨论的话题。

function countdown(n) {

首先,我们来谈谈函数。
无处不在的函数表达式

while(n–>0)     // “n goes to zero”

JavaScript
一个有趣的特点是,任何时候你需要一个函数,你可以很方便地创建它们。

alert(n);

例如,为一个按钮绑定点击事件:

blastoff();

$("#confetti-btn").click(

$("#confetti-btn").click(

}

jQuery 的 .click()
方法需要一个函数作为参数,我们可以很方便地就地创建一个函数:

上面这段代码可以正常运行,循环会一直重复直到n趋于0,这当然不是ES6中的新特性,它只不过是将两个你早已熟悉的特性通过一些误导性的手段结合在一起。你能理解么?通常来说,类似这种谜团都可以在Stack
Overflow上找到答案。

$("#confetti-btn").click(function (event) {
 playTrumpet();
 fireConfettiCannon();
});


$("#confetti-btn").click(function (event) {
 playTrumpet();
 fireConfettiCannon();
});

当然,同样地,小于等于操作符<=也形似箭头,你可以在JS代码、隐藏的图片样式中找到更多类似的箭头,但是我们就不继续寻找了,你应该注意到我们漏掉了一种特殊的箭头。

现在对我们来说,编写这样的代码是最自然的事了。但是在 JavaScript
流行起来之前,这种风格的代码看起来还是有些奇怪,因为在其他语言中都没有这样的特性。在
1958 年,Lisp 就有了函数表达式,也叫 lambda 函数,而在存在多年的
C++、Python、C# 和 Java 中没有该特性。

  <!– 单行注释

现在,这四门语言都有了 lambda 表达式,而且新出现的语言都普遍内置了
lambda 表达式。如今 JavaScript 也支持该特性了,这必须感谢那些重度依赖
lambda 表达式的库的开发者,这推动了该特性被广泛采纳。

–> “趋向于”操作符

与其他几门语言相比,JavaScript 的语法略显冗长:

<= 小于等于

// A very simple function in six languages.
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

// A very simple function in six languages.
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());


// ES5
var selected = allJobs.filter(function (job) {
 return job.isSelected();
});

// ES6
var selected = allJobs.filter(job => job.isSelected());

函数表达式无处不在

当你需要只有一个参数的函数,箭头函数的语法可以简化为 Identifier =>
Expression,直接省略了 function 和 return
关键字,连括号和结尾的分号也同时省略了。

JavaScript中有一个有趣的特性,无论何时,当你需要一个函数时,你都可以在想添加的地方输入这个函数。

编写一个有多个(或没有参数,或 Rest
参数和参数默认值,或解构参数)参数的函数,你需要用括号将参数括起来:

举个例子,假设你尝试告诉浏览器用户点击一个特定按钮后的行为,你会这样写:

// ES5
var total = values.reduce(function (a, b) {
 return a + b;
}, 0);

// ES6
var total = values.reduce((a, b) => a + b, 0);

// ES5
var total = values.reduce(function (a, b) {
 return a + b;
}, 0);

// ES6
var total = values.reduce((a, b) => a + b, 0);

$(“#confetti-btn”).click(

箭头函数还可以与一些工具函数库完美地配合使用,比如 Underscore.js 和
Immutable,事实上,Immutable 文档中的例子全部都是使用 ES6
编写,其中有很多已经使用到了箭头函数。

jQuery的.click()方法接受一个参数:一个函数。没问题,你可以在这里输入一个函数:

函数体除了使用一个表达式外,箭头函数还可以包含一个语句块,回忆之前我们提到过的例子:

$(“#confetti-btn”).click(function(event){playTrumpet();fireConfettiCannon();});

// ES5
$("#confetti-btn").click(function (event) {
 playTrumpet();
 fireConfettiCannon();
});

// ES5
$("#confetti-btn").click(function (event) {
 playTrumpet();
 fireConfettiCannon();
});


于现在的我们来说,写出这样的代码相当自然,而回忆起在这种编程方式流行之前,这种写法相对陌生一些,许多语言中都没有这种特性。1958年,Lisp首
先支持函数表达式,也支持调用lambda函数,而C++,Python、C#以及Java在随后的多年中一直不支持这样的特性。

下面是采用箭头函数的写法:

现在截然不同,所有的四种语言都已支持lambda函数,更新出现的语言普遍都支持内建的lambda函数。我们必须要感谢JavaScript和早期的JavaScript程序员,他们勇敢地构建了重度依赖lambda函数的库,让这种特性被广泛接受。

// ES6
$("#confetti-btn").click(event => {
 playTrumpet();
 fireConfettiCannon();
});

// ES6
$("#confetti-btn").click(event => {
 playTrumpet();
 fireConfettiCannon();
});

令人伤感的是,随后在所有我提及的语言中,只有JavaScript的lambda的语法最终变得冗长乏味。

需要注意的是,使用语句块的箭头函数不会自动返回一个值,必须显式地使用
return 来返回一个值。

// 六种语言中的简单函数示例

还有一个忠告,当使用箭头函数来返回一个对象时,始终使用括号将返回的对象括起来:

function(a){returna>0;}// JS

// create a new empty object for each puppy to play with
var chewToys = puppies.map(puppy => {});  // BUG!
var chewToys = puppies.map(puppy => ({})); // ok


// create a new empty object for each puppy to play with
var chewToys = puppies.map(puppy => {});  // BUG!
var chewToys = puppies.map(puppy => ({})); // ok

[](int a){returna>0;}// C++

因为空对象 {} 与空语句块 {} 看上去一模一样,ES6 将始终把紧跟在 =>
后面的 { 当作语句块的开始,而不是一个对象的开始,那么 puppy => {}
就被解析为一个没有函数体的箭头函数,而且返回值为 undefined。

(lambda(a)(>a0));;Lisp

您可能感兴趣的文章:

  • JavaScript中的普通函数和箭头函数的区别和用法详解
  • javascript
    ES6中箭头函数注意细节小结
  • 深入理解Javascript箭头函数中的this
  • Javascript中 带名 匿名
    箭头函数的重要区别(推荐)
  • 详解Javascript ES6中的箭头函数(Arrow
    Functions)
  • 浅析JavaScript 箭头函数 generator Date
    JSON
  • JavaScript箭头函数_动力节点Java学院整理

lambda a:a>0# Python

a=>a>0// C#

a->a>0// Java

箭袋中的新羽

ES6中引入了一种编写函数的新语法

// ES5

var selected=allJobs.filter(function(job){

returnjob.isSelected();

});

// ES6

var selected = allJobs.filter(job=>job.isSelected());

当你只需要一个只有一个参数的简单函数时,可以使用新标准中的箭头函数,它的语法非常简单:标识符=>表达式。你无需输入function和return,一些小括号、大括号以及分号也可以省略。

(我个人对于这个特性非常感激,不再需要输入function这几个字符对我而言至关重要,因为我总是不可避免地错误写成functoin,然后我就不得不回过头改正它。)

如果要写一个接受多重参数(也可能没有参数,或者是不定参数、默认参数、参数解构)的函数,你需要用小括号包裹参数list。

// ES5

artotal=values.reduce(function(a,b){

returna+b;

},0);

// ES6

var total=values.reduce((a,b)=>a+b,0);

我认为这看起来酷毙了。

正如你使用类似Underscore.js和Immutable.js这样的库提供的函数工具,箭头函数运行起来同样美不可言。事实上,Immutable的文档中的示例全都由ES6写成,其中的许多特性已经用上了箭头函数。

那么不是非常函数化的情况又如何呢?除表达式外,箭头函数还可以包含一个块语句。回想一下我们之前的示例:

// ES5

$(“#confetti-btn”).click(function(event){

playTrumpet();

fireConfettiCannon();

});

这是它们在ES6中看起来的样子:

// ES6

$(“#confetti-btn”).click(event=>{

playTrumpet();

fireConfettiCannon();

});

这是一个微小的改进,对于使用了Promises的代码来说箭头函数的效果可以变得更加戏剧性,}).then(function
(result) {这样的一行代码可以堆积起来。

注意,使用了块语句的箭头函数不会自动返回值,你需要使用return语句将所需值返回。

小提示:当使用箭头函数创建普通对象时,你总是需要将对象包裹在小括号里。

// 为与你玩耍的每一个小狗创建一个新的空对象

var chewToys=puppies.map(puppy=>{});// 这样写会报Bug!

var chewToys=puppies.map(puppy=>({}));//

用小括号包裹空对象就可以了。

不幸的是,一个空对象{}和一个空的块{}看起来完全一样。ES6中的规则是,紧随箭头的{被解析为块的开始,而不是对象的开始。因此,puppy
=> {}这段代码就被解析为没有任何行为并返回undefined的箭头函数。

更令人困惑的是,你的JavaScript引擎会将类似{key:
value}的对象字面量解析为一个包含标记语句的块。幸运的是,{是唯一一个有歧义的字符,所以用小括号包裹对象字面量是唯一一个你需要牢记的小窍门。

这个函数的this值是什么呢?

普通function函数和箭头函数的行为有一个微妙的区别,箭头函数没有它自己的this值,箭头函数内的this值继承自外围作用域。

在我们尝试说明这个问题前,先一起回顾一下。

JavaScript中的this是如何工作的?它的值从哪里获取?这些问题的答案可都不简单,如果你对此倍感清晰,一定因为你长时间以来一直在处理类似的问题。

这个问题经常出现的其中一个原因是,无论是否需要,function函数总会自动接收一个this值。你是否写过这样的hack代码:

{

addAll:functionaddAll(pieces){

varself=this;

_.each(pieces,function(piece){

self.add(piece);

});

},

}

在这里,你希望在内层函数里写的是this.add(piece),不幸的是,内层函数并未从外层函数继承this的值。在内层函数里,this会是window或undefined,临时变量self用来将外部的this值导入内部函数。(另一种方式是在内部函数上执行.bind(this),两种方法都不甚美观。)

在ES6中,不需要再hackthis了,但你需要遵循以下规则:

通过object.method()语法调用的方法使用非箭头函数定义,这些函数需要从调用者的作用域中获取一个有意义的this值。

其它情况全都使用箭头函数。

// ES6

{

addAll:functionaddAll(pieces){

_.each(pieces,piece=>this.add(piece));

},

}

在ES6的版本中,注意addAll方法从它的调用者处获取了this值,内部函数是一个箭头函数,所以它继承了外围作用域的this值。

超赞的是,在ES6中你可以用更简洁的方式编写对象字面量中的方法,所以上面这段代码可以简化成:

// ES6的方法语法

{

addAll(pieces){

_.each(pieces,piece=>this.add(piece));

},

}

在方法和箭头函数之间,我再也不会错写functoin了,这真是一个绝妙的设计思想!

箭头函数与非箭头函数间还有一个细微的区别,箭头函数不会获取它们自己的arguments对象。诚然,在ES6中,你可能更多地会使用不定参数和默认参数值这些新特性。

借助箭头函数洞悉计算机科学的风尘往事

我们已经讨论了许多箭头函数的实际用例,它还有一种可能的使用方法:将ES6箭头函数作为一个学习工具,来深入挖掘计算的本质,是否实用,终将取决于你自己。

1936年,Alonzo Church和Alan
Turing各自开发了强大的计算数学模型,图灵将他的模型称为a-machines,但是每一个人都称其为图灵机。Church写的是函数模型,他的模型被称为lambda演算(λ-calculus)。这一成果也被Lisp借鉴,用LAMBDA来指示函数,这也是为何我们现在将函数表达式称为lambda函数。

但什么是lambda演算呢?“计算模型”又意味着什么呢?


几句话解释清楚很难,但是我会努力阐释:lambda演算是第一代编程语言的一种形式,但毕竟存储程序计算机在十几二十年后才诞生,所以它原本不是为编程
语言设计的,而是为了表达任意你想到的计算问题设计的一种极度简化的纯数学思想的语言。Church希望用这个模型来证明普遍意义的计算。

最终他发现,在他的系统中只需要一件东西:函数。

这种声明方式无与伦比,不借助对象、数组、数字、if语句、while循环、分号、赋值、逻辑运算符甚或是事件循环,只须使用函数就可以从0开始重建JavaScript能实现的每一种计算。

这是用Church的lambda标记写出来的数学家风格的“程序”示例:

fix = λf.(λx.f(λv.x(x)(v)))(λx.f(λv.x(x)(v)))

等效的JavaScript函数是这样的:

varfix=f=>(x=>f(v=>x(x)(v)))

                (x=>f(v=>x(x)(v)));

所以,在JavaScript中实现了一个可以运行的lambda演算,它根植于这门语言中。

Alonzo
Church和lambda演算后继研究者们的故事,以及它是如何潜移默化地入驻每一门主流编程语言的,已经远超本文的讨论范围。但是如果你对计算机科学
的奠基感兴趣,或者你只是对一门只用函数就可以做许多类似循环和递归这样的事情的语言倍感兴趣,你可以在一个下雨的午后深入邱奇数(Church
numerals)和不动点组合子(Fixed-point
combinator),在你的Firefox控制台或Scratchpad中仔细研究一番。结合ES6的箭头函数以及其它强大的功能,JavaScript称得上是一门探索lambda演算的最好的语言。

我何时可以使用箭头函数?

早在2013年,我就在Firefox中实现了ES6箭头函数的功能,Jan de
Mooij为其优化加快了执行速度。感谢Tooru
Fujisawa以及ziyunfei(译者注:中国开发者,为Mozilla作了许多贡献)后续打的补丁。

微软Edge预览版中也实现了箭头函数的功能,如果你想立即在你的Web项目中使用箭头函数,可以使用Babel、Traceur或TypeScript,这三个工具均已实现相关功能。