迭代器高级功能

使用迭代器的基本功能,并使用生成器来方便地创建迭代器,你就已经可以完成很多工作了。然而,在单纯迭代集合的值之外的任务中,迭代器会显得更加强大。在 ES6 的开发过程中,许多独特的思想与模式出现了,激励着规范制定者去添加更多的功能。这些附加功能可能很细微,但将它们结合使用就能形成一些有趣的互动。

传递参数给迭代器

本章中的范例已经展示了迭代器能够将值传递出来,通过 next() 方法或者在生成器中使用 yield 都可以。但你还能通过 next() 方法向迭代器传递参数。当一个参数被传递给 next() 方法时,该参数就会成为生成器内部 yield 语句的值。这种能力对于更多高级功能(例如异步编程)来说是非常重要的。此处有个基本范例:

function *createIterator() {
    let first = yield 1;
    let second = yield first + 2;       // 4 + 2
    yield second + 3;                   // 5 + 3
}

let iterator = createIterator();

console.log(iterator.next());           // "{ value: 1, done: false }"
console.log(iterator.next(4));          // "{ value: 6, done: false }"
console.log(iterator.next(5));          // "{ value: 8, done: false }"
console.log(iterator.next());           // "{ value: undefined, done: true }"

对于 next() 的首次调用是一个特殊情况,传给它的任意参数都会被忽略。由于传递给 next() 的参数会成为 yield 语句的值,该 yield 语句指的是上次生成器中断执行处的语句;而 next() 方法第一次被调用时,生成器函数才刚刚开始执行,没有所谓的 “上一次中断处的 yield 语句” 可供赋值。因此在第一次调用 next() 时,不存在任何向其传递参数的理由。

由于传递给 next() 的参数会成为 yield 语句的值,则首次调用给 next() 提供的参数就只会替换生成器函数中的第一个 yield 语句;但若要在生成器内部使用该值,又要求它在此 yield 语句之前就必须能被访问到,而这是不可能的。

但译者不赞成这种说法,因此对这段表述进行了修改。

在第二次调用 next() 时,4 作为参数被传递进去,这个 4 最终被赋值给了生成器函数内部的 first 变量。在包含赋值操作的第一个 yield 语句中,表达式右侧在第一次调用 next() 时被计算,而表达式左侧则在第二次调用 next() 方法时、并在生成器函数继续执行前被计算。由于第二次调用 next() 传入了 4,这个值就被赋给了 first 变量,之后生成器继续执行。

第二个 yield 使用了第一个 yield 的结果并加上了 2,也就是返回了一个 6。当 next() 被第三次调用时,传入了参数 5。这个值被赋给了 second 变量,并随后用在了第三个 yield 语句中,返回了 8

通过考虑在生成器函数内部每次继续运行时都执行了什么代码,会有助于思考到底发生了什么。示意图 8-1 用颜色演示了代码在 yield 之前是如何执行的。

image 2024 05 18 20 18 04 289
Figure 1. 示意图 8-1: 在一个生成器内部的代码执行

黄色表示对于 next() 的第一次调用、以及在生成器内部执行的所有代码;水蓝色表示了对 next(4) 的调用以及随之执行的代码;而紫色则表示对 next(5) 的调用以及随之执行的代码。棘手的部分在于:在左侧代码执行之前,右侧的各个表达式是如何执行与停止的。这使得调试错综复杂的生成器要比调试正规函数更麻烦一些。

你目前已看到了当一个值传递给 next() 方法时,yield 的表现就好像 return 一样。然而,这并不是在一个生成器内部仅能使用的执行技巧,你还可以让迭代器抛出一个错误。

在迭代器中抛出错误

能传递给迭代器的不仅是数据,还可以是错误条件。迭代器可以选择实现一个 throw() 方法,用于指示迭代器应在恢复执行时抛出一个错误。这是对异步编程来说很重要的一个能力,同时也会增加生成器内部的灵活度,能够既模仿返回一个值,又模仿抛出错误(也就是退出函数的两种方式)。你可以传递一个错误对象给 throw() 方法,当迭代器继续进行处理时应当抛出此错误。例如:

function *createIterator() {
    let first = yield 1;
    let second = yield first + 2;       // yield 4 + 2, then throw
    yield second + 3;                   // never is executed
}

let iterator = createIterator();

console.log(iterator.next());                   // "{ value: 1, done: false }"
console.log(iterator.next(4));                  // "{ value: 6, done: false }"
console.log(iterator.throw(new Error("Boom"))); // error thrown from generator

在本例中,前两个 yield 表达式照常被运算,但当 throw() 被调用时,一个错误在 let second 运算之前就被抛出了。这有效停止了代码执行,类似于直接抛出错误,其中唯一区别是错误在何处被抛出。示意图 8-2 演示了每一步所执行的是什么代码。

image 2024 05 18 20 19 17 752
Figure 2. 示意图 8-2: 在一个生成器内部抛出错误

在此示意图中,红色表示当 throw() 被调用时所执行的代码,红星说明了错误在生成器内部大约何时被抛出。前两个 yield 语句被执行之后,当调用 throw() 时,在任何其他代码执行之前错误就被抛出了。

了解这些之后,你就可以在生成器内部使用一个 try-catch 块来捕捉这种错误:

function *createIterator() {
    let first = yield 1;
    let second;

    try {
        second = yield first + 2;       // yield 4 + 2, then throw
    } catch (ex) {
        second = 6;                     // on error, assign a different value
    }
    yield second + 3;
}

let iterator = createIterator();

console.log(iterator.next());                   // "{ value: 1, done: false }"
console.log(iterator.next(4));                  // "{ value: 6, done: false }"
console.log(iterator.throw(new Error("Boom"))); // "{ value: 9, done: false }"
console.log(iterator.next());                   // "{ value: undefined, done: true }"

本例使用一个 try-catch 块包裹了第二个 yield 语句。尽管这个 yield 自身的执行不会出错,但在对 second 变量赋值之前,错误就在此时被抛出,于是 catch 部分捕捉错误并将这个变量赋值为 6,然后再继续执行到下一个 yield 处并返回了 9

要注意一件有趣的事情发生了:throw() 方法就像 next() 方法一样返回了一个结果对象。由于错误在生成器内部被捕捉,代码继续执行到下一个 yield 处并返回了下一个值,也就是 9

next()throw() 都当作迭代器的指令,会有助于思考。next() 方法指示迭代器继续执行(可能会带着给定的值),而 throw() 方法则指示迭代器通过抛出一个错误继续执行。在调用点之后会发生什么,根据生成器内部的代码来决定。

next()throw() 方法控制着迭代器在使用 yield 时内部的执行,但你也可以使用 return 语句。不过 return 的工作方式与它在正规函数中有一些差异,正如你将在下一节中看到的那样。

生成器的 Return 语句

由于生成器是函数,你可以在它内部使用 return 语句,既可以让生成器早一点退出执行,也可以指定在 next() 方法最后一次调用时的返回值。在本章大多数例子中,对迭代器上的 next() 的最后一次调用都返回了 undefined,但你还可以像在其他函数中那样,使用 return 来指定另一个返回值。在生成器内,return 表明所有的处理已完成,因此 done 属性会被设为 true,而如果提供了返回值,就会被用于 value 字段。此处有个例子,单纯使用 return 让生成器更早返回:

function *createIterator() {
    yield 1;
    return;
    yield 2;
    yield 3;
}

let iterator = createIterator();

console.log(iterator.next());           // "{ value: 1, done: false }"
console.log(iterator.next());           // "{ value: undefined, done: true }"

此代码中的生成器在一个 yield 语句后跟随了一个 return 语句。这个 return 表明将不会再有任何值,也因此剩余的 yield 语句就不会再执行(它们是不可到达的) 。

你也可以指定一个返回值,会被用于最终返回的结果对象中的 value 字段。例如:

function *createIterator() {
    yield 1;
    return 42;
}

let iterator = createIterator();

console.log(iterator.next());           // "{ value: 1, done: false }"
console.log(iterator.next());           // "{ value: 42, done: true }"
console.log(iterator.next());           // "{ value: undefined, done: true }"

此处,当第二次调用 next() 方法时,值 42 被返回在 value 字段中,此时 done 字段的值才第一次变为了 true。第三次调用 next() 返回了一个对象,其 value 属性再次变回 undefined,你在 return 语句中指定的任意值都只会在结果对象中出现一次,此后 value 字段就会被重置为 undefined

扩展运算符与 for-of 循环会忽略 return 语句所指定的任意值。一旦它们看到 done 的值为 true,它们就会停止操作而不会读取对应的 value 值。不过,在生成器进行委托时,迭代器的返回值会非常有用。

生成器委托

在某些情况下,将两个迭代器的值合并器一起会更有用。生成器可以用星号(*)配合 yield 这一特殊形式来委托其他的迭代器。正如生成器的定义,星号出现在何处是不重要的,只要落在 yield 关键字与生成器函数名之间即可。此处有个范例:

function *createNumberIterator() {
    yield 1;
    yield 2;
}

function *createColorIterator() {
    yield "red";
    yield "green";
}

function *createCombinedIterator() {
    yield *createNumberIterator();
    yield *createColorIterator();
    yield true;
}

var iterator = createCombinedIterator();

console.log(iterator.next());           // "{ value: 1, done: false }"
console.log(iterator.next());           // "{ value: 2, done: false }"
console.log(iterator.next());           // "{ value: "red", done: false }"
console.log(iterator.next());           // "{ value: "green", done: false }"
console.log(iterator.next());           // "{ value: true, done: false }"
console.log(iterator.next());           // "{ value: undefined, done: true }"

此例中的 createCombinedIterator() 生成器依次委托了 createNumberIterator()createColorIterator()。返回的迭代器从外部看来就是一个单一的迭代器,用于产生所有的值。每次对 next() 的调用都会委托给合适的生成器,直到使用 createNumberIterator()createColorIterator() 创建的迭代器全部清空为止。然后最终的 yield 会被执行以返回 true

生成器委托也能让你进一步使用生成器的返回值。这是访问这些返回值的最简单方式, 并且在执行复杂任务时会非常有用。例如:

function *createNumberIterator() {
    yield 1;
    yield 2;
    return 3;
}

function *createRepeatingIterator(count) {
    for (let i=0; i < count; i++) {
        yield "repeat";
    }
}

function *createCombinedIterator() {
    let result = yield *createNumberIterator();
    yield *createRepeatingIterator(result);
}

var iterator = createCombinedIterator();

console.log(iterator.next());           // "{ value: 1, done: false }"
console.log(iterator.next());           // "{ value: 2, done: false }"
console.log(iterator.next());           // "{ value: "repeat", done: false }"
console.log(iterator.next());           // "{ value: "repeat", done: false }"
console.log(iterator.next());           // "{ value: "repeat", done: false }"
console.log(iterator.next());           // "{ value: undefined, done: true }"

此处 createCombinedIterator() 生成器委托了 createNumberIterator() 并将它的返回值赋值给了 result 变量。由于 createNumberIterator() 包含 return 3 语句,该返回值就是 3result 变量接下来会作为参数传递给 createRepeatingIterator() 生成器,指示同一个字符串需要被重复几次(在本例中是三次) 。

注意值 3 从未在对于 next() 方法的任何调用中被输出。当前它仅仅存在于 createCombinedIterator() 生成器内部。但你也可以通过添加另一个 yield 语句来输出这个值,正如:

function *createNumberIterator() {
    yield 1;
    yield 2;
    return 3;
}

function *createRepeatingIterator(count) {
    for (let i=0; i < count; i++) {
        yield "repeat";
    }
}

function *createCombinedIterator() {
    let result = yield *createNumberIterator();
    yield result;
    yield *createRepeatingIterator(result);
}

var iterator = createCombinedIterator();

console.log(iterator.next());           // "{ value: 1, done: false }"
console.log(iterator.next());           // "{ value: 2, done: false }"
console.log(iterator.next());           // "{ value: 3, done: false }"
console.log(iterator.next());           // "{ value: "repeat", done: false }"
console.log(iterator.next());           // "{ value: "repeat", done: false }"
console.log(iterator.next());           // "{ value: "repeat", done: false }"
console.log(iterator.next());           // "{ value: undefined, done: true }"

在此代码中,额外的 yield 语句明确地将 createNumberIterator() 生成器的返回值进行了输出。

使用返回值的生成器委托是一种非常强大的范式,能引出一些非常有趣的应用可能, 尤其是在用于与异步操作结合时。

你可以直接在字符串上使用 yield * (例如 yield * "hello"),字符串的默认迭代器会被使用。