ES6 深入 是一个系列文章,介绍了在 ECMAScript 标准的第 6 版(简称 ES6)中添加到 JavaScript 编程语言的新特性。
我对今天的文章感到兴奋。今天,我们将讨论 ES6 中最神奇的功能。
我所说的“神奇”是什么意思?首先,这个特性与 JS 中已经存在的事物非常不同,以至于一开始可能看起来完全神秘。从某种意义上说,它颠覆了语言的正常行为!如果那不是魔法,我不知道什么才是。
不仅如此:此特性简化代码和理清“回调地狱”的能力几乎超自然。
我是不是有点夸大了?让我们深入了解一下,你可以自己判断。
介绍 ES6 生成器
什么是生成器?
让我们先看一个例子。
<pre>
function* quips(name) {
yield "hello " + name + "!";
yield "i hope you are enjoying the blog posts";
if (name.startsWith("X")) {
yield "it's cool how your name starts with X, " + name;
}
yield "see you later!";
}
</pre>
这是一些用于 会说话的猫 的代码,可能是当今互联网上最重要的应用类型。(继续,点击链接,与猫咪互动。当你彻底困惑时,回到这里获取解释。)
它看起来有点像函数,对吧?这被称为生成器函数,它与函数有很多共同之处。但是你可以立即看到两个区别
-
普通函数以
function
开头。生成器函数以function*
开头。 -
在生成器函数内部,
yield
是一个关键字,语法类似于return
。不同之处在于,虽然函数(即使是生成器函数)只能返回一次,但生成器函数可以产生任意次数。yield
表达式暂停生成器的执行,以便稍后可以恢复执行。
所以就是这样,这就是普通函数和生成器函数之间的最大区别。普通函数无法暂停自身。生成器函数可以。
生成器做什么
当你调用quips()
生成器函数时会发生什么?
<pre>
> var iter = quips("jorendorff");
[object Generator]
> iter.next()
{ value: "hello jorendorff!", done: false }
> iter.next()
{ value: "i hope you are enjoying the blog posts", done: false }
> iter.next()
{ value: "see you later!", done: false }
> iter.next()
{ value: undefined, done: true }
</pre>
你可能非常习惯普通函数及其行为。当你调用它们时,它们会立即开始运行,并且会一直运行,直到它们返回或抛出异常。所有这些对于任何 JS 程序员来说都是第二天性。
调用生成器看起来完全一样:quips("jorendorff")
。但是当你调用生成器时,它不会立即开始运行。相反,它会返回一个已暂停的生成器对象(在上面的示例中称为iter
)。你可以将此生成器对象视为一个函数调用,冻结在时间中。具体来说,它冻结在生成器函数的顶部,在运行其第一行代码之前。
每次调用生成器对象的.next()
方法时,函数调用都会自行解冻并运行,直到到达下一个yield
表达式。
这就是为什么我们每次调用上面的iter.next()
时,都会得到一个不同的字符串值。这些是由quips()
主体中的yield
表达式产生的值。
在最后一次iter.next()
调用中,我们终于到达了生成器函数的末尾,因此结果的.done
字段为true
。到达函数的末尾就像返回undefined
一样,这就是为什么结果的.value
字段为undefined
。
现在可能是回到 会说话的猫演示页面 并真正玩弄代码的好时机。尝试在循环中放置一个yield
。会发生什么?
从技术角度讲,每次生成器产生值时,其栈帧(局部变量、参数、临时值以及生成器主体中执行的当前位置)都会从栈中移除。但是,生成器对象会保留对(或复制)此栈帧的引用,以便稍后的.next()
调用可以重新激活它并继续执行。
值得指出的是,**生成器不是线程。**在具有线程的语言中,多段代码可以同时运行,通常会导致竞争条件、不确定性和出色的性能。生成器根本不像那样。当生成器运行时,它在与调用方相同的线程中运行。执行顺序是顺序的和确定性的,绝不是并发的。与系统线程不同,生成器仅在其主体中由yield
标记的位置暂停。
好的。我们知道生成器是什么了。我们已经看到一个生成器运行、暂停自身,然后恢复执行。现在是最大的问题。这种奇怪的能力可能有什么用?
生成器是迭代器
上周,我们看到 ES6 迭代器不仅仅是一个内置类。它们是语言的扩展点。你只需实现两个方法:[Symbol.iterator]()
和.next()
,就可以创建自己的迭代器。
但是实现接口总归至少需要一些工作。让我们看看在实践中迭代器实现是什么样的。例如,让我们创建一个简单的range
迭代器,它只是从一个数字计数到另一个数字,就像一个老式的 C for (;;)
循环。
<pre>
// 这应该“叮”三次
for (var value of range(0, 3)) {
alert("Ding! at floor #" + value);
}
</pre>
这是一个解决方案,使用 ES6 类。(如果class
语法不是很清楚,别担心——我们将在未来的博文中介绍它。)
<pre>
class RangeIterator {
constructor(start, stop) {
this.value = start;
this.stop = stop;
}
[Symbol.iterator]() { return this; }
next() {
var value = this.value;
if (value < this.stop) {
this.value++;
return {done: false, value: value};
} else {
return {done: true, value: undefined};
}
}
}
// 返回一个新的迭代器,从 'start' 计数到 'stop'。
function range(start, stop) {
return new RangeIterator(start, stop);
}
</pre>
这就是在 Java 或 Swift 中实现迭代器的方式。它还不错。但它也不是微不足道的。这段代码中是否存在任何错误?不容易说。它看起来与我们试图在这里模拟的原始for (;;)
循环完全不同:迭代器协议迫使我们拆分循环。
此时,你可能对迭代器感觉有点不冷不热。它们可能非常棒使用,但它们似乎很难实现。
你可能不会想到建议我们向 JS 语言引入一种疯狂的、令人费解的新控制流结构,仅仅是为了使迭代器更容易构建。但既然我们确实有生成器,我们能否在这里使用它们?让我们试试看
<pre>
function* range(start, stop) {
for (var i = start; i < stop; i++)
yield i;
}
</pre>
上面的 4 行生成器替换了之前 23 行的range()
实现,包括整个RangeIterator
类。这是可能的,因为**生成器是迭代器。**所有生成器都内置实现了.next()
和[Symbol.iterator]()
。你只需编写循环行为。
在没有生成器的情况下实现迭代器就像被迫用完全被动语态写一封很长的电子邮件。当简单地说出你的意思不是一种选择时,你最终说出的内容可能会变得非常复杂。RangeIterator
很长很奇怪,因为它必须描述循环的功能,而无需使用循环语法。生成器是答案。
我们还能如何使用生成器充当迭代器?
-
**使任何对象可迭代。**只需编写一个遍历
this
的生成器函数,在遍历过程中产生每个值。然后将该生成器函数安装为对象的[Symbol.iterator]
方法。 -
**简化数组构建函数。**假设你有一个每次被调用时都返回结果数组的函数,如下所示
<pre>
// 将一维数组 'icons'
// 分成长度为 'rowLength' 的数组。
function splitIntoRows(icons, rowLength) {
var rows = [];
for (var i = 0; i < icons.length; i += rowLength) {
rows.push(icons.slice(i, i + rowLength));
}
return rows;
}
</pre>生成器使这种代码稍微简短一些
<pre>
function* splitIntoRows(icons, rowLength) {
for (var i = 0; i < icons.length; i += rowLength) {
yield icons.slice(i, i + rowLength);
}
}
</pre>唯一的行为差异是,它不是一次计算所有结果并返回它们的数组,而是返回一个迭代器,并且结果按需逐个计算。
-
**不寻常大小的结果。**你无法构建无限数组。但是你可以返回一个生成无限序列的生成器,并且每个调用者都可以根据需要从中提取任意数量的值。
-
**重构复杂的循环。**你是否有一个庞大而丑陋的函数?你想将其分成两个更简单的部分吗?生成器是添加到重构工具包中的一把新刀。当你遇到复杂的循环时,可以将产生数据的代码部分提取出来,将其变成一个单独的生成器函数。然后将循环更改为
for (var data of myNewGenerator(args))
。 -
**用于处理可迭代对象的工具。**ES6不提供用于过滤、映射和一般处理任意可迭代数据集的扩展库。但是生成器非常适合使用几行代码构建所需的工具。
例如,假设你需要一个
Array.prototype.filter
的等效项,它适用于 DOM NodeList,而不仅仅是数组。小菜一碟<pre>
function* filter(test, iterable) {
for (var item of iterable) {
if (test(item))
yield item;
}
}
</pre>
那么生成器有用吗?当然。它们是实现自定义迭代器的一种极其简单的方法,而迭代器是 ES6 中数据和循环的新标准。
但生成器不仅仅可以做到这些。它甚至可能不是它们最重要的用途。
生成器和异步代码
这是一段我之前写的一些 JS 代码。
<pre>
};
})
});
});
});
});
</pre>
你可能在自己的代码中见过类似的东西。异步 API 通常需要回调,这意味着每次执行操作时都需要编写一个额外的匿名函数。因此,如果你有一段执行三件事的代码,而不是三行代码,你将看到三个缩进级别的代码。
这是我写的一些 JS 代码
<pre>
}).on('close', function () {
done(undefined, undefined);
}).on('error', function (error) {
done(error);
});
</pre>
异步 API 具有错误处理约定,而不是异常。不同的 API 具有不同的约定。在大多数情况下,错误默认情况下会被静默丢弃。在某些情况下,即使是普通的成功完成也会默认被丢弃。
到目前为止,这些问题仅仅是我们为异步编程付出的代价。我们已经接受了异步代码看起来不如相应的同步代码简洁明了的事实。
生成器为我们带来了新的希望,表明情况不必如此。
Q.async() 是一种实验性的尝试,它使用生成器和 Promise 来生成类似于相应同步代码的异步代码。例如
<pre>
// 同步代码来制造一些噪音。
function makeNoise() {
shake();
rattle();
roll();
}
// 异步代码来制造一些噪音。
// 返回一个 Promise 对象,当我们完成制造噪音时,该对象将被解析。
function makeNoise_async() {
return Q.async(function* () {
yield shake_async();
yield rattle_async();
yield roll_async();
yield roll_async();
});
}
</pre>
主要区别在于,异步版本必须在调用异步函数的每个位置添加yield
关键字。
在Q.async
版本中添加像if
语句或try
/catch
块这样的复杂结构,就像在普通的同步版本中添加它们一样。与其他编写异步代码的方式相比,这感觉不像是学习一门全新的语言。
如果你已经看到这里了,你可能会喜欢James Long关于这个主题的非常详细的文章。
所以生成器为我们指明了通往一种新的异步编程模型的道路,这种模型似乎更适合人类的大脑。这项工作还在进行中。其中,更好的语法可能会有所帮助。异步函数提案,建立在Promise和生成器的基础上,并借鉴了C#中类似的功能,正在被纳入ES7的考虑范围。
我什么时候可以使用这些神奇的东西?
在服务器端,你今天就可以在io.js中使用ES6生成器(如果你使用--harmony
命令行选项,也可以在Node中使用)。
在浏览器中,目前只有Firefox 27+和Chrome 39+支持ES6生成器。要在网络上使用生成器,你需要使用Babel或Traceur将你的ES6代码转换为Web友好的ES5代码。
一些值得感谢的人:生成器最初由Brendan Eich在JS中实现;他的设计紧密遵循了Python生成器,而Python生成器则受到Icon的启发。它们在Firefox 2.0中发布早在2006年。标准化的道路是崎岖的,语法和行为也随之改变了一些。ES6生成器由编译器黑客Andy Wingo在Firefox和Chrome中实现。这项工作由彭博社赞助。
yield;
关于生成器,还有更多内容需要说明。我们没有涵盖.throw()
和.return()
方法,.next()
的可选参数,或者yield*
表达式语法。但我认为这篇文章已经足够长和令人困惑了。就像生成器本身一样,我们应该暂停一下,下次再继续讨论剩下的内容。
但下周,让我们稍微改变一下方向。我们已经连续讨论了两个深入的主题。谈论一个不会改变你生活的ES6特性难道不是很好吗?一些简单而明显有用的东西?一些会让你微笑的东西?ES6也有一些这样的特性。
即将推出:一个可以直接应用到你每天编写的代码中的特性。请在下周加入我们,深入了解ES6模板字符串。
14 条评论