深入 ES6:箭头函数

ES6 In Depth 是一个系列,介绍了在 ECMAScript 第 6 版(简称为 ES6)中添加到 JavaScript 编程语言的新功能。

箭头一直是 JavaScript 从一开始就有的部分。第一个 JavaScript 教程建议将内联脚本包装在 HTML 注释中。这将防止不支持 JS 的浏览器错误地将你的 JS 代码显示为文本。你将编写如下代码

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

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

为了支持这个奇怪的技巧,浏览器中的 JavaScript 引擎将字符 `<!--` 视为单行注释的开始。不是开玩笑。这确实是语言的一部分,并且一直有效,不仅仅是在内联 `<script>` 的顶部,而是在 JS 代码的任何地方。它甚至在 Node 中也起作用。

碰巧,这种注释风格在 ES6 中首次被标准化。 但这不是我们要讨论的箭头。

箭头序列 `-->` 也表示单行注释。奇怪的是,在 HTML 中, `-->` 之前的字符是注释的一部分,但在 JS 中, `-->` 之后的行其余部分是注释。

它变得更加奇怪。此箭头仅在出现在行首时才表示注释。这是因为在其他上下文中,`-->` 是 JS 中的运算符,“转到”运算符!

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

此代码确实有效。 循环运行直到 `var n` 达到 0。这也不是 ES6 中的新功能,而是熟悉功能的组合,以及一些误导。你能弄清楚这里发生了什么吗?和往常一样,谜题的答案可以在 Stack Overflow 上找到

当然,还有小于或等于运算符 `<=`。也许你可以在你的 JS 代码中找到更多箭头,类似于“隐藏图片”风格,但让我们在这里停止,并观察到缺少一个箭头

<!-- 单行注释
--> “转到”运算符
<= 小于或等于
=> ???

`=>` 发生了什么?今天,我们找到了答案。

首先,让我们谈谈函数。

函数表达式无处不在

JavaScript 的一个有趣功能是,无论何时你需要一个函数,你都可以直接在运行代码的中间键入该函数。

例如,假设你正在尝试告诉浏览器在用户点击特定按钮时该怎么做。你开始输入

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

jQuery 的 `.click()` 方法接受一个参数:一个函数。没问题。你可以在此处直接键入一个函数

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

现在,编写这样的代码对我们来说很自然。因此,回顾一下,在 JavaScript 推广这种编程方式之前,许多语言没有这个功能。当然,Lisp 在 1958 年就有了函数表达式,也称为lambda 函数。但 C++、Python、C# 和 Java 都存在多年没有它们。

不再是了。这四种语言现在都有 lambda。较新的语言普遍内置了 lambda。我们要感谢 JavaScript - 以及早期无畏地构建依赖于 lambda 的库的 JavaScript 程序员,这导致了该功能的广泛采用。

因此,稍微有点令人难过的是,在我提到的所有语言中,JavaScript 的 lambda 语法最终成为了最冗长的。

// 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(<strong>function (job) {
  return job.isSelected();
}</strong>);

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

当只需要一个简单函数(带有一个参数)时,新的箭头函数语法就是 `<i>标识符</i> => <i>表达式</i>`。你可以跳过键入 `function` 和 `return`,以及一些括号、大括号和分号。

(我个人对这个功能非常感谢。不用键入 `function` 对我来说很重要,因为我总是无意中键入 `functoin`,然后不得不回去纠正。)

要编写一个带有多个参数(或没有参数,或 剩余参数或默认值,或 解构 参数)的函数,你需要在参数列表周围添加括号。

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

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

我认为它看起来很漂亮。

箭头函数与库提供的函数式工具(如 Underscore.jsImmutable)配合得很好。事实上,Immutable 文档 中的示例都是用 ES6 编写的,因此其中许多示例已经使用了箭头函数。

非函数式设置呢?箭头函数可以包含一个语句块,而不仅仅是一个表达式。回顾一下我们之前的示例

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

以下是它在 ES6 中的样子

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

一个小的改进。对使用 Promise 的代码的影响可能更大,因为 `}).then(function (result) {` 行会堆积起来。

请注意,带块体的箭头函数不会自动返回值。请使用 `return` 语句来实现。

在使用箭头函数创建普通对象时,有一个注意事项。始终将对象用括号括起来

// 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}` 的对象字面量看起来就像一个包含带标签语句的块 - 至少对你的 JavaScript 引擎来说是这样。幸运的是,`{` 是唯一一个模棱两可的字符,所以将对象字面量用括号括起来是你要记住的唯一技巧。

`this` 是什么?

普通 `function` 函数和箭头函数之间存在一个细微的行为差异。箭头函数没有自己的 `this` 值。 箭头函数内部的 `this` 值始终是从封闭作用域继承的。

在我们尝试弄清楚这在实践中意味着什么之前,让我们先退一步。

JavaScript 中的 `this` 如何工作?它的值从哪里来?没有简短的答案。 如果它在你脑海中看起来很简单,那是因为你已经处理它很久了!

这个问题经常出现的一个原因是,`function` 函数会自动接收一个 `this` 值,无论它们是否需要它。你是否曾经写过这个技巧?

{
  ...
  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` 值的函数。
  • 对其他所有内容使用箭头函数。
// ES6
{
  ...
  addAll: function addAll(pieces) {
    _.each(pieces, piece => this.add(piece));
  },
  ...
}

在 ES6 版本中,请注意 `addAll` 方法从其调用者那里接收 `this`。内部函数是一个箭头函数,因此它从封闭作用域继承 `this`。

作为奖励,ES6 还提供了一种更简短的方式来在对象字面量中编写方法!因此,上面的代码可以进一步简化

// ES6 with method syntax
{
  ...
  addAll(pieces) {
    _.each(pieces, piece => this.add(piece));
  },
  ...
}

在方法和箭头之间,我可能再也不会键入 `functoin` 了。这是一个美好的想法。

箭头函数和非箭头函数之间还有一个细微的差别:箭头函数也没有自己的 `arguments` 对象。当然,在 ES6 中,你可能更愿意使用剩余参数或默认值。

使用箭头穿透计算机科学的黑暗核心

我们已经讨论了箭头函数的许多实际用途。还有一个可能的用例我想谈谈:ES6 箭头函数作为一种学习工具,来揭示关于计算本质的一些深刻的东西。这是否实用,你必须自己决定。

1936 年,Alonzo Church 和 Alan Turing 独立开发了强大的计算数学模型。Turing 将他的模型称为a-机器,但每个人都立即开始称它们为图灵机。Church 则写的是关于函数的。他的模型被称为 λ 演算。(λ 是希腊字母 lambda 的小写形式。)这项工作是 Lisp 使用 `LAMBDA` 来表示函数的原因,这就是为什么我们今天将函数表达式称为“lambda”的原因。

但什么是 λ 演算?“计算模型”究竟是什么意思?

很难用几句话解释清楚,但这是我的尝试:λ 演算是最早的编程语言之一。它并非设计为一种编程语言 - 毕竟,存储程序计算机还要再过一二十年才会出现 - 而是一种极其简单、简化的、纯粹的数学语言概念,可以表达你想要执行的任何类型的计算。Church 希望通过这个模型来证明关于计算本身的一些事情。

他发现他只需要他的系统中的一件事:函数

想想这个说法是多么非凡。没有对象,没有数组,没有数字,没有 `if` 语句,`while` 循环,分号,赋值,逻辑运算符,或事件循环,就可以从头开始使用函数重新构建 JavaScript 可以执行的任何类型的计算。

以下是一个数学家可以使用 Church 的 λ 符号编写的“程序”示例

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

等效的 JavaScript 函数如下所示

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

也就是说,JavaScript 包含了一个实际上运行的 λ 演算的实现。λ 演算就在 JavaScript 中。

Alonzo Church 和后来的研究人员用 λ 演算做的事情,以及它如何悄无声息地渗透到几乎所有主要的编程语言中,这些都超出了这篇博文的范围。但如果你对计算机科学的基础感兴趣,或者你只是想看看一个只有函数的语言如何做循环和递归,那么你可以花一个雨天午后看看 Church 数不动点组合子,并在你的 Firefox 控制台或 Scratchpad 中使用它们。JavaScript 凭借 ES6 箭头和其他优势,可以合理地宣称自己是探索 λ 演算的最佳语言。

我什么时候可以使用箭头?

ES6 箭头函数是我在 2013 年为 Firefox 实现的。Jan de Mooij 使其速度更快。感谢 Tooru Fujisawa 和 ziyunfei 的补丁。

箭头函数也已在 Microsoft Edge 预览版中实现。它们也适用于 BabelTraceurTypeScript,如果你有兴趣立即在 Web 上使用它们。

我们的下一个主题是 ES6 中一个更奇怪的特性。我们将看到 typeof x 返回一个全新的值。我们将问:什么时候一个名字不是字符串?我们将对等式的含义感到困惑。这将很奇怪。所以请在下周加入我们,深入了解 ES6 符号。

关于 Jason Orendorff

更多 Jason Orendorff 的文章...


21 条评论

  1. Kyle Simpson

    我认为这里没有提到关于箭头函数的几件事,应该提到。

    1. 所有新的 ES6 参数形式,如默认值、解构、剩余参数等......所有这些都强制你在参数列表周围使用 ( ),即使只使用一个参数。

    2. 箭头函数是匿名的,这意味着您将没有一个自绑定的词法标识符来进行自引用,用于递归、事件处理程序解绑等。是的,ES6 有“名称推断”,但这只是为函数对象分配一个内部 .name 属性 - 它不提供词法上的帮助。

    混合了一些轻松的、善意的讽刺,我已经将箭头的所有决策路径都放入此流程图中

    https://github.com/getify/You-Dont-Know-JS/blob/master/es6%20&%20beyond/fig1.png

    2015 年 6 月 4 日 下午 5:34

    1. Simeon Vincent

      FWIW,我对极简语法并不感冒。正如流程图所说,x = y => y; 不是最易读的。

      也就是说,我 *确实* 发现 x = (y) => { return y; }; 很清楚。尤其是在回调或承诺中使用时。

      承诺
      .then((resp) => {
      return resp.prop;
      })
      .catch((err) => {
      console.error(err);
      })

      2015 年 6 月 4 日 下午 11:31

      1. 约翰

        x = (y) => { return y; };

        这怎么清楚呢?我还没有读过这篇文章(是的,我先读评论 ;-),但这是我第一次看到一些我无法立即理解的 JavaScript 代码。我读到的是,某个变量 x 等于某个变量 y(虽然我不确定 y 周围的括号是什么意思),x 和 y 等于或大于返回 y 的函数。什么?这绝不是清晰的。

        2015 年 6 月 12 日 上午 9:51

        1. Bergo

          比较是 `>=`/“ :-)

          2015 年 6 月 23 日 下午 8:31

          1. Bergi

            啊,评论中的 html 语法。我的意思是写

            比较是 >=/<=,不是 => :-)

            2015 年 6 月 24 日 上午 9:59

      2. Bergi

        嗯,显然
        promise.then(resp => resp.prop).catch(console.error)
        比冗长的函数表达式更易读?

        2015 年 6 月 23 日 下午 8:30

  2. Blaise Kal

    您可以在此处找到详细的浏览器/平台支持表:http://kangax.github.io/compat-table/es6/#arrow_functions

    2015 年 6 月 5 日 上午 1:40

  3. Julian Lamb

    Jason,你的 ES6 深入文章很棒。继续努力!

    2015 年 6 月 5 日 下午 2:26

  4. Karthick Siva

    感谢你发布的这篇文章,jason。

    “箭头函数和非箭头函数之间还有一个细微的差别:箭头函数也没有自己的 arguments 对象。”

    为什么?你能详细说明一下吗?

    2015 年 6 月 6 日 上午 5:55

    1. rbzl

      因为'arguments'也像'this'一样是词法绑定的

      2015 年 6 月 7 日 上午 4:35

    2. Jason Orendorff

      标准使箭头函数尽可能多地从封闭上下文中继承内容。

      该规则涵盖了 `this` 和 `arguments`,以及一些与子类化相关的其他值,我们将在后面讨论:`new.target` 和 `super` 使用的值。普通函数和方法对所有这些都有自己的值;箭头函数从上下文中继承所有这些值。

      2015 年 6 月 7 日 上午 5:08

  5. 基思

    嗯...我从未听说过“转到”运算符(-->)。

    可能是 n --> 0 实际上只是 n-- > 0(n 减减大于零)?

    2015 年 6 月 7 日 上午 10:52

    1. Jason Orendorff

      当然可以。(这就是文章提到的“误导”;如果你点击进入 StackOverflow 答案,一切都会揭晓。)

      2015 年 6 月 8 日 下午 1:08

  6. Karthick Siva

    这澄清了我的疑问。谢谢,期待未来的文章。

    2015 年 6 月 7 日 下午 9:03

  7. Mörre Noseshine

    > 那么,稍微让人难过的是,在我提到的所有语言中,JavaScript 的 lambda 语法最终是最冗长的。

    我觉得不同且经常相反的力量同时作用于 JavaScript,并且都取得了成功,这很有趣:一方面是那些对他们必须输入的每个字母感到恼火的人。另一方面是编写 API 的人,他们来自想要在函数名称中完整描述函数正在做什么的阵营,例如 URL.createObjectURL、getElementsByClassName、addEventLIstener 等。因此核心语言将字符数减少到最小,但 API 过去和现在仍在朝着相反的方向发展。

    就我个人而言,我处于中间。我发现“纯数学”语法,就像文章中 Church 的 lambda 表示法示例一样,每个字母都有其意义,非常令人疲倦地解析(“通过头部”),但具有所有这些 looooooong 名称的源代码也同样令人疲倦,用于经常使用的微不足道的功能。

    2015 年 6 月 11 日 上午 5:13

    1. thinsoldier

      DOM Api 与语言并不完全相同。你可以在 php 或 python 中操作 DOM,并且你必须在 php 或 python 中使用相同的 api 名称(getElementsByClassName 等),因为它是 DOM 标准的实现。

      2015 年 6 月 11 日 上午 11:55

      1. Mörre Noseshine

        > DOM Api 与语言并不完全相同。

        我没有说它是一样的。我清楚地知道其中的区别。感谢您免费提供关于基础 Web 开发的教育。我同样渴望学习自然数的乘法,我希望将来能掌握它……

        2015 年 6 月 11 日 上午 12:07

    2. Kyle Simpson

      @Mörre

      我认为你关于 API 名称过长的观点是有效的,但我可以建议一些来自实际 JS 语言本身的观点,而不是你列出的那些实际上来自其他标准机构的观点。我认为这会使你的观点更有力

      Object.getOwnPropertyNames
      Object.getOwnPropertySymbols
      Object.getOwnPropertyDescriptor
      Object.propertyIsEnumerable

      2015 年 6 月 12 日 上午 11:02

  8. Mörre Noseshine

    @Kyle 是的,谢谢!虽然我知道(正如我在上面所说,有点生气)一个是 DOM,另一个是 JS,但我并不关心 *谁* 负责(我没有指责任何人,我认为?),我的观点是我们实际上看到了什么。在我的代码中,对 DOM 操作函数的调用(不仅限于浏览器端),或“原始 JS”函数,或任何其他 API 包括 npm 安装的模块和其他库,看起来都一样,并且可以愉快地混合使用。

    我的观点是要指出 - 没有明确的意图 - 在同一代码中混合使用极端的简洁和非常冗长的函数名称。这很奇怪。

    好吧,也许我的观点是,指出“在这里我们可以节省 5 个字符!”在实践中并不完全相关。我认为应该记住,道格拉斯·克罗克福德在他的关于 JavaScript 的精彩视频之一中,作为一个旁注,指出很多开发人员花费大量精力来节省几个按键,而实际键入代码的时间是最少的。

    2015 年 6 月 12 日 上午 11:19

    1. Jason Orendorff

      这基本上是一个合理的反对意见。什么时候简洁值得付出这种努力?

      考虑一下 Smalltalk 或 Ruby。如果代码块用 17 个额外的字符编写,这些语言将完全不同,并且非常混乱,对吧?它们将难以阅读,使用起来也不愉快。

      JavaScript 正在成为这样一种语言。函数表达式被用于数组处理、将代码附加到用户输入事件、I/O、应用程序内部各部分之间的松散耦合通信、异步控制流等等,而且这仅仅是个开始。函数表达式无处不在。它们应该拥有适合其使用方式的语法。

      2015 年 6 月 16 日 下午 05:10

  9. Bergi

    吹毛求疵:你的 `this` 示例不是最好的。我们不再想使用 Underscore 的 `_.each` 了!
    ES5 代码
    pieces.forEach(this.add, this);
    ES6 代码
    for (let piece of pieces) this.add(piece);

    也许你可以使用事件监听器,比如
    this.el.onclick = e => this.add(e.timestamp);
    等等

    2015 年 6 月 23 日 下午 20:39

本文评论已关闭。