ES6 in Depth: Arrow Funtions

ES6 in Depth: Arrow funtions

箭头是JS每个开始的一部分,JS的第一个指导说可以在 HTML 注释中内置脚本代码,这会导致浏览器错误地认为JS代码是文本,从而不能运行JS。你可能会编写代码如下:

1
2
3
4
5
\<script language="javascript"> // \ for md safe
  <!--
     document.bgColor = "brown"; // red
  // -->
</script>

旧的浏览器只会看到两个不支持的标签和一个注释,只有新的浏览器会看到 JS 代码。

为了支持这奇怪的代码,你浏览器中的 JS 引擎会将 <!-- 认为是单行注释的开始。没有开玩笑,这至始至终是JS语言的一部分,而且至今仍然是可用的。这不仅仅可以在内置的 <script>的顶部,还可以在 JS 的任何地方,它甚至在 Node 也是能工作的。

由此,ES6 的注释风格第一次的标准化,但这并不是我们这里所谈到的箭头。

序列箭头-->也表示一个单行的注释,奇怪的是,HTML认为在-->之前的部分是注释,而JS认为-->之后剩余部分是注释。

这确实很怪异。这箭头只有当其出现在行的开头时才会认为是注释。这是因为在其它的内容中,-->是一个JS的操作符,“去到”操作符。

1
2
3
4
5
function countdown(n) {
  while (n --> 0) // "n goes to zero"
     alert(n);
  blastoff();
}

这代码确实可以运行链接, 这个循环运行会从 n 到 0 。这也不是ES6的一个新特性,它其实是类似箭头的操作组合,这确实会有些误导。你能猜出指出是什么吗?和平常一样,这答案的迷云可以在 Stack Overflow 中得到。

好了,<=表示小于等于操作符,你可以找出JS代码中更多的这些箭头。细想一下有哪些箭头,但是现在先暂停一下,观察一下一个不认识的箭头:

1
2
3
4
<!-- single-line comment
--> goes to operator
<=  less than or equal to
=>  ???

=>会发现什么呢?今天,让我们研究一下。

首先,让我们稍微讨论一下 函数。

无处不在的函数表达式

JS中一个有趣的特性是可以在任何时候添加你所需要的函数,你只需要在运行代码的中部编写合适的函数。

例如,你可以试图告诉浏览器当用户点击具体的按钮时做什么,你会开始编写:

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

jQuery 的 .click()方法需要一个参数:一个函数。没问题,你仅需要编写合适的函数,如下:

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

现在的我们已经很自然地编写出这类似的代码了,细想一下,这写法在JS流行之前是显得奇怪的,很多的编程语言并不会有这一特性。在1958年,Lisp 有其函数表达式,它叫 lambda 函数。但是,C++、Python、C# 和 Java 没有函数也生存了很多年。

这情况不会再有了,这四种语言现在也有 lambda 函数。更新的语言普遍都会内置有 lambda 函数。我们 JS 也很感谢 —- 早期 JS 编程人员大胆地构建大量的 lambda 库,从而使得这特性广泛地被使用。

稍微有些伤心的是,在我提到过的所有编程语言中,JS的lambda语法证明是最为冗长的。

1
2
3
4
5
6
7
// 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 介绍编写函数的新的语法:

1
2
3
4
5
6
7
// ES5
var selected = allJobs.filter(function (job) {
  return job.isSelected();
});

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

当你仅是需要个简单的一个参数的函数,这新的箭头函数语法只需要简单地 Identifier => Expression。你可以不用编写 functionreturn这些关键字,同时包括 括号、大括号、分号。

(我个人是非常喜欢这个特性的,没有限定键入function对我来说十分的重要,因为我时不时会键入functoin,从而不得不回头去更正它。)

为了编写多参数的函数(或者没有参数、剩余和默认参数、或解构参数),你将需要添加括号和参数列表。

1
2
3
4
5
6
7
// ES5
var total = values.reduce(function (a, b) {
  return a + b;
}, 0);

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

我认为它看起来是十分好的。

箭头函数可以像某些库中的功能性工具(functional tool)一样漂亮地工作,如 Underscore.js 和 Immutable。实际上,Immutable的文档 中的例子都是用ES6来编写的,所以其中的一些已经使用上了 箭头函数。

那么,对于非功能性的设置该怎么办呢?箭头函数 可以包含着一个代码块,而不仅是一个表达式。回顾一下我们之前的例子:

1
2
3
4
5
// ES5
$("#confetti-btn").click(function (event) {
  playTrumpet();
  fireConfettiCannon();
});

在ES6中如下:

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

一个小提升,在使用 Promises 时,这作用会更加有魔法,它可以将类如 }).then( function (result) {的行堆起来。

注意,代码块的箭头函数并不会自动返回一个值,所以需要 return 进行返回值操作。

当使用箭头函数来创建普通对象时,需要用括号将对象括起来:

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

不幸的是,一个空对象和一个空的代码块实际上是一样的。在 ES6 中,箭头后面的 { 会总是被认为是 代码块的开始标识,而不是对象的开始。因此,代码 puppy => {}会被编译为一个箭头函数,并不会做什么,返回值为 undefined

更多的困扰,一个对象从字面中如 {key: value} 至少看起来像一包含了 标签语句的代码块,但这要看你的JS引擎怎么识别它。幸运的是,{ 是唯一的歧义的字符,所以使用括号来包含对象 是 唯一 你需要谨记的怪点。

this 是什么

普通的 function函数和箭头函数有些微小的差别。箭头函数没有它们自有的 this 的值。箭头函数中的 this 总是来源于包含此箭头函数的作用域。

在我们试图在实践中指出上面意味着什么之前,让我们稍微后退一下。

JS 中的 this 是怎么工作的?它的值来源于哪里?这并没有简短的答案。如果你头脑认为它很简单,是因为你已经处理它很长时间了。

引起 this 这提问的原因是 function 函数会自动接收一个 this的值,无论他们是否想要它。你是否编写过这黑代码:

1
2
3
4
5
6
7
8
9
10
{
  ...
  addAll: function addAll(pieces) {
    var self = this;
    _.each(pieces, function (piece) {
      self.add(piece);
    });
  },
  ...
}

这里,你可能会喜欢在内部的函数中编写 this.add(piece)。但是不幸,内部函数并不会继承外部函数的this值。在内部函数中,this将可能会是window或者undefined。临时变量self作用是将外部函数中的this传递到内部函数中。(另外的方法是在内部使用 .bind(this),但是两种方法都不是很漂亮。)

在 ES6 中,如果你按照以下规则使用,这种this的黑代码将会消失:

  • 当调用非箭头函数的方法时,使用 object.method()语法进行调用,如此这函数会从调用者中接收到一个有意义的this的值。
  • 在任何地方都使用箭头函数
1
2
3
4
5
6
7
8
// ES6 with method syntax
{
  ...
  addAll(pieces) {
    _.each(pieces, piece => this.add(piece));
  },
  ...
}

通过 方法箭头,我不再会编写 functoin了,这真是个很好的想法。

箭头和非箭头函数还有一个细小的差别:箭头函数并不会获取他们自己的 arguments对象。同时,在ES6中,你最好在任何的地方都使用剩余和默认参数。

使用箭头可穿透黑科技的核心

我们已经谈到很多箭头函数的实践应用,我还想提到一个可能会用到的点:把 ES6 中的箭头函数作为一个学习的工具,去挖掘计算领域深层次的东西。无论其是否实用,这都取决于你自己。

在1936年,Alonzo Church 和 Alan Turing 各自独立开发了强大的的数学计算模型。Turing 称之为 a-machines,但是大家从开始都称为 图灵机器(Turing machines)。Church 编写的是想取代函数,他的模型称为 λ-calculus。 (λ 是希腊语中的lambda的小写。) 这就是Lisp使用关键字 LAMBDA来声明函数的原因,这也是为什么我们今天称函数为 “lambda”的原因。

但是,什么是 λ-calculus ? 计算模型(model of computation) 表示什么呢?

简短的文字是很难解释的,但是这里是我的理解:λ-calculus 是第一种编程语言之一。虽然它并不是被设计为一个编程语言,随后的一到二十年存储程序的计算机也随之出现。但是,它作为一个非常简单、纯粹的数学语言,可以展示出你想要的各种计算。Church 希望这个模型能广泛地用来证明计算。

然后,他发现仅需要在他的系统中有:函数(functions)。

想象一下这种声明是怎么特别的:没有对象、数组,没有数值,没有if语句、while循环,没有分号、分配符号、逻辑操作符,或者任意的循环,白手起家,只使用函数能够做到 JS 做到的任何类型的计算操作。

下面是个数学算法的程序,使用了 Church λ 的符号:

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

它等价于JS中的

1
2
var fix = f =>  (x => f(v => x(x)(v)))
                       (x => f(v => x(x)(v)));

如此,JS就包含了 λ-calculus (lambda积分)的实现,并且是能够运行。The λ-calculus is in JavaScript

关于 Alonzo Church 和之后的研究人员对 λ-calculus 做什么的故事,还有其是怎么潜移默化地影响着每个主流编程语言,这已经超出了这篇文章讨论的范围了。但是,如果你对基础计算科感兴趣,或者你仅仅是想看到一编程语言没有其它、只有函数是怎么做到循环和递归的,你可能要努力并花费些下雨的下午去查看 Church numeralsfixed-point combinators, 然后将代码运行于 Firefox 的控制台或者 Scratchpad 。由于 ES6 的箭头 拥有其它力量,JS可以光明地声称它是探索 λ-calculus 的最好编程语言。

什么时候使用箭头(箭头函数)

ES6 中的箭头函数在Firefox是由我还实现的,于2013年。Jan de Mooij 的工作是其更为快速,同时感谢 Tooru Fujisawa 和 ziyunfei 的补丁。

箭头函数也在微软的Edge浏览器的预览版本中支持了,它们也可以在 Babel , Traceur 和 TypeScript 中使用了,如此你现在可以使用它们(从而使用箭头函数)。

我们的下个主题是ES6中比较特殊的一个特性。我们可以看到typeof x会返回一个完成新的值。那么我们会问:什么时候返回的不是string?我们将会努力寻找其相等的含义,确实有些怪异。那么,请于下周加入我们,继续深入 ES6 吧。

Comments