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.js 和 Immutable)配合得很好。事实上,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 预览版中实现。它们也适用于 Babel、Traceur 和 TypeScript,如果你有兴趣立即在 Web 上使用它们。
我们的下一个主题是 ES6 中一个更奇怪的特性。我们将看到 typeof x
返回一个全新的值。我们将问:什么时候一个名字不是字符串?我们将对等式的含义感到困惑。这将很奇怪。所以请在下周加入我们,深入了解 ES6 符号。
21 条评论