我们将要稍稍深入一下 迭代器 的话题,但我们会绕回到它们如何与generator关联,并使用generator来 生成 值。

    想象你正在生产一系列的值,它们中的每一个都与前一个值有可定义的关系。为此,你将需要一个有状态的发生器来记住上一个给出的值。

    你可以用函数闭包(参加本系列的 作用域与闭包)来直接地实现这样的东西:

    注意: 这里的计算逻辑已经被简化了,但从概念上讲,直到 下一次 gimmeSomething()调用发生之前,我们不想计算 下一个值(也就是nextVal),因为一般对于持久性更强的,或者比简单的number更有限的资源的发生器来说,那可能是一种资源泄漏的设计。

    生成随意的数字序列不是是一个很真实的例子。但是如果你从一个数据源中生成记录呢?你可以想象很多相同的代码。

    事实上,这种任务是一种非常常见的设计模式,通常用迭代器解决。一个 迭代器 是一个明确定义的接口,用来逐个通过一系列从发生器得到的值。迭代器的JS接口,和大多数语言一样,是在你每次想从发生器中得到下一个值时调用的next()

    我们可以为我们的数字序列发生器实现标准的 迭代器

    1. var something = (function(){
    2. var nextVal;
    3. return {
    4. // `for..of`循环需要这个
    5. [Symbol.iterator]: function(){ return this; },
    6. // 标准的迭代器接口方法
    7. next: function(){
    8. if (nextVal === undefined) {
    9. nextVal = 1;
    10. }
    11. else {
    12. nextVal = (3 * nextVal) + 6;
    13. }
    14. return { done:false, value:nextVal };
    15. }
    16. };
    17. })();
    18. something.next().value; // 1
    19. something.next().value; // 9
    20. something.next().value; // 33
    21. something.next().value; // 105

    注意: 我们将在“Iterables”一节中讲解为什么我们在这个代码段中需要[Symbol.iterator]: ..这一部分。在语法上讲,两个ES6特性在发挥作用。首先,[ .. ]语法称为一个 计算型属性名(参见本系列的 this与对象原型)。它是一种字面对象定义方法,用来指定一个表达式并使用这个表达式的结果作为属性名。另一个,Symbol.iterator是ES6预定义的特殊Symbol值。

    next()调用返回一个对象,它带有两个属性:done是一个boolean值表示 迭代器 的完成状态;value持有迭代的值。

    ES6还增加了for..of循环,它意味着一个标准的 迭代器 可以使用原生的循环语法来自动地被消费:

    1. for (var v of something) {
    2. console.log( v );
    3. // 不要让循环永无休止!
    4. if (v > 500) {
    5. break;
    6. }
    7. // 1 9 33 105 321 969

    注意: 因为我们的something迭代器总是返回done:false,这个for..of循环将会永远运行,这就是为什么我们条件性地放进一个break。对于迭代器来说永不终结是完全没有问题的,但是也有一些情况 迭代器 将运行在有限的值的集合上,而最终返回done:true

    for..of循环为每一次迭代自动调用next()——他不会给next()传入任何值——而且他将会在收到一个done:true时自动终结。这对于在一个集合的数据中进行循环十分方便。

    当然,你可以手动循环一个迭代器,调用next()并检查done:true条件来知道什么时候停止:

    1. for (
    2. var ret;
    3. (ret = something.next()) && !ret.done;
    4. ) {
    5. console.log( ret.value );
    6. // 不要让循环永无休止!
    7. if (ret.value > 500) {
    8. break;
    9. }
    10. // 1 9 33 105 321 969

    除了制造你自己的 迭代器 之外,许多JS中(就ES6来说)内建的数据结构,比如array,也有默认的 迭代器

    for..of循环向a要来它的迭代器,并自动使用它迭代a的值。

    注意: 看起来像是一个ES6的奇怪省略,普通的object有意地不带有像array那样的默认 迭代器。原因比我们要在这里讲的深刻得多。如果你想要的只是迭代一个对象的属性(不特别保证顺序),Object.keys(..)返回一个array,它可以像for (var k of Object.keys(obj)) { ..这样使用。像这样用for..of循环一个对象上的键,与用for..in循环内很相似,除了在for..in中会包含[[Prototype]]链的属性,而Object.keys(..)不会(参见本系列的 this与对象原型)。

    在我们运行的例子中的something对象被称为一个 迭代器,因为它的接口中有next()方法。但一个紧密关联的术语是 iterable,它指 包含有 一个可以迭代它所有值的迭代器的对象。

    在ES6中,从一个 iterable 中取得一个 迭代器 的方法是,iterable 上必须有一个函数,它的名称是特殊的ES6符号值Symbol.iterator。当这个函数被调用时,它就会返回一个 迭代器。虽然不是必须的,但一般来说每次调用应当返回一个全新的 迭代器

    前一个代码段的a就是一个 iterablefor..of循环自动地调用它的Symbol.iterator函数来构建一个 迭代器。我们当然可以手动地调用这个函数,然后使用它返回的 iterator

    1. var a = [1,3,5,7,9];
    2. var it = a[Symbol.iterator]();
    3. it.next().value; // 1
    4. it.next().value; // 3
    5. it.next().value; // 5
    6. ..

    在前面定义something的代码段中,你可能已经注意到了这一行:

    1. [Symbol.iterator]: function(){ return this; }

    这段有点让人困惑的代码制造了something值——something迭代器 的接口——也是一个 iterable;现在它既是一个 iterable 也是一个 迭代器。然后,我们把something传递给for..of循环:

    1. for (var v of something) {
    2. ..
    3. }

    for..of循环期待something是一个 iterable,所以它会寻找并调用它的Symbol.iterator函数。我们将这个函数定义为简单地return this,所以它将自己给出,而for..of不会知道这些。

    带着 迭代器 的背景知识,让我们把注意力移回generator。一个generator可以被看做一个值的发生器,我们通过一个 迭代器 接口的next()调用每次从中抽取一个值。

    所以,一个generator本身在技术上讲并不是一个 iterable,虽然很相似——当你执行generator时,你就得到一个 迭代器

    我们可以用generator实现早前的something无限数字序列发生器,就像这样:

    1. function *something() {
    2. var nextVal;
    3. while (true) {
    4. if (nextVal === undefined) {
    5. nextVal = 1;
    6. }
    7. else {
    8. nextVal = (3 * nextVal) + 6;
    9. yield nextVal;
    10. }
    11. }

    注意: 在一个真实的JS程序中含有一个while..true循环通常是一件非常不好的事情,至少如果它没有一个breakreturn语句,那么它就很可能永远运行,并同步地,阻塞/锁定浏览器UI。然而,在generator中,如果这样的循环含有一个yield,那它就是完全没有问题的,因为generator将在每次迭代后暂停,yield回主程序和/或事件轮询队列。说的明白点儿,“generator把while..true带回到JS编程中了!”

    不仅是更简单的代码——我们不必自己制造 迭代器 接口了——它实际上是更合理的代码,因为它更清晰地表达了意图。比如,while..true循环告诉我们这个generator将要永远运行——只要我们一直向它请求,它就一直 产生 值。

    现在我们可以在for..of循环中使用新得发亮的*something()generator了,而且你会看到它工作起来基本一模一样:

    1. for (var v of something()) {
    2. console.log( v );
    3. // 不要让循环永无休止!
    4. if (v > 500) {
    5. break;
    6. }
    7. }
    8. // 1 9 33 105 321 969

    不要跳过for (var v of something()) ..!我们不仅仅像之前的例子那样将something作为一个值引用了,而是调用*something()generator来得到它的 迭代器,并交给for..of使用。

    如果你仔细观察,在这个generator和循环的互动中,你可能会有两个疑问:

    • 为什么我们不能说for (var v of something) ..?因为这个something是一个generator,而不是一个 iterable。我们不得不调用something()来构建一个发生器给for..of,以便它可以迭代。
    • something()调用创建一个 迭代器,但是for..of想要一个 iterable,对吧?对,generator的 迭代器 上也有一个Symbol.iterator函数,这个函数基本上就是return this,就像我们刚才定义的somethingiterable。换句话说generator的 迭代器 也是一个 iterable

    停止Generator

    在前一个例子中,看起来在循环的break被调用后,*something()generator的 迭代器 实例基本上被留在了一个永远挂起的状态。

    但是这里有一个隐藏的行为为你处理这件事。for..of循环的“异常完成”(“提前终结”等等)——一般是由breakreturn,或未捕捉的异常导致的——会向generator的 迭代器 发送一个信号,以使它终结。

    注意: 技术上讲,for..of循环也会在循环正常完成时向 迭代器 发送这个信号。对于generator来说,这实质上是一个无实际意义的操作,因为generator的 迭代器 要首先完成,for..of循环才能完成。然而,自定义的 迭代器 可能会希望从for..of循环的消费者那里得到另外的信号。

    虽然一个for..of循环将会自动发送这种信号,你可能会希望手动发送信号给一个 迭代器;你可以通过调用return(..)来这么做。

    如果你在generator内部指定一个try..finally从句,它将总是被执行,即便是generator从外部被完成。这在你需要进行资源清理时很有用(数据库连接等):

    1. function *something() {
    2. try {
    3. var nextVal;
    4. while (true) {
    5. if (nextVal === undefined) {
    6. nextVal = 1;
    7. }
    8. else {
    9. nextVal = (3 * nextVal) + 6;
    10. }
    11. yield nextVal;
    12. }
    13. }
    14. // 清理用的从句
    15. finally {
    16. console.log( "cleaning up!" );
    17. }
    18. }

    前面那个在for..of中带有break的例子将会触发finally从句。但是你可以用return(..)从外部来手动终结generator的 迭代器 实例:

    当我们调用it.return(..)时,它会立即终结generator,从而运行finally从句。而且,它会将返回的value设置为你传入return(..)的任何东西,这就是Hellow World如何立即返回来的。我们现在也不必再包含一个break,因为generator的 迭代器 会被设置为done:true,所以循环会在下一次迭代时终结。

    generator的命名大部分源自于这种 消费生产的值 的用法。但要重申的是,这只是generator的用法之一,而且坦白的说,在这本书的背景下这甚至不是我们主要关注的。

    但是现在我们更加全面地了解它们的机制是如何工作的,我们接下来可以将注意力转向generator如何实施于异步并发。