第 12 章 迭代器和生成器

可迭代对象及其关联的迭代器是 ES6 的一个特性,在本书中我们已经多次看到。数组(包括 typedarray)是可迭代的,字符串、Set 和 Map 对象也是如此。这意味着这些数据结构的内容可以被 for/of 循环遍历,就像我们在 §5.4.4 中看到的那样:

let sum = 0;
for(let i of [1,2,3]) { // Loop once for each of these values
    sum += i;
}
sum   // => 6

迭代器还可以用 ... 运算符将可迭代对象展开或“扩展”到数组初始化或函数调用中,如 §7.1.2 所示:

let chars = [..."abcd"]; // chars == ["a", "b", "c", "d"]
let data = [1, 2, 3, 4, 5];
Math.max(...data)        // => 5

迭代器可以与析构赋值一起使用:

let purpleHaze = Uint8Array.of(255, 0, 255, 128);
let [r, g, b, a] = purpleHaze; // a == 128

当你迭代一个 Map 对象时,返回的值是 [key, value] 对,这在 for/of 循环的解构赋值中很好用:

let m = new Map([["one", 1], ["two", 2]]);
for(let [k,v] of m) console.log(k, v); // Logs 'one 1' and 'two 2'

如果只迭代键或只迭代值而不是一对,可以使用 keys() 和 values() 方法:

[...m]            // => [["one", 1], ["two", 2]]: default iteration
[...m.entries()]  // => [["one", 1], ["two", 2]]: entries() method is the same
[...m.keys()]     // => ["one", "two"]: keys() method iterates just map keys
[...m.values()]   // => [1, 2]: values() method iterates just map values

最后,通常用于数组对象的许多内置函数和构造函数实际上被编写(在ES6及以后版本中)为接受任意实参的迭代器。Set() 构造函数就是这样一种API:

// Strings are iterable, so the two sets are the same:
new Set("abc") // => new Set(["a", "b", "c"])

本章说明了迭代器是如何工作的,并演示了如何创建自己的可迭代的数据结构。在说明了基本的迭代器之后,本章将介绍生成器,这是 ES6 的一个强大的新特性,它是一种特别简单的方法创建迭代器。

12.1 迭代器是如何工作的

for/of 循环和展开运算符可与可迭代对象无缝配合,但是值得了解使迭代工作的实际情况。需要了解三种独立的类型才能理解 JavaScript 中的迭代。首先,可迭代的对象:可以迭代的是诸如 Array,Set 和 Map 之类的类型。其次,迭代器对象本身,它执行迭代。第三,一个迭代结果对象,该对象保存迭代的每个步骤的结果。

任何对象具有特殊迭代器方法,并且该方法返回迭代器对象,那么该对象为可迭代对象。迭代器对象具有 next() 方法,该方法返回迭代结果对象。迭代结果对象是具有名为 value 和 done 的属性的对象。要迭代一个可迭代的对象,首先要调用其迭代器方法以获取一个迭代器对象。然后,重复调用迭代器对象的 next() 方法,直到返回的值的 done 属性设置为 true。棘手的事情是,可迭代对象的迭代器方法没有常规名称,而是使用 Symbol Symbol.iterator 作为其名称。因此,也可以用很复杂的方式编写可迭代对象的简单 for/of 循环,如下所示:

let iterable = [99];
let iterator = iterable[Symbol.iterator]();
for(let result = iterator.next(); !result.done; result = iterator.next()) {
    console.log(result.value)  // result.value == 99
}

内置可迭代数据类型的迭代器对象本身是可迭代的。(也就是说,它具有一个名为 Symbol.iterator 的方法,该方法会自行返回。)当要通过“部分使用”的迭代器进行迭代时,以下代码中有时会很有用:

let list = [1,2,3,4,5];
let iter = list[Symbol.iterator]();
let head = iter.next().value;  // head == 1
let tail = [...iter];          // tail == [2,3,4,5]

12.2 可迭代对象的实现

可迭代对象在 ES6 中非常有用,应该考虑使自己的数据类型在可以表示迭代的任何时候都可迭代。第 9 章示例 9-2 和 9-3 中显示的 Range 类是可迭代的。这些类使用生成器函数使其可迭代。我们将在本章稍后介绍生成器,但首先,我们将再次实现 Range 类,使其无需依赖生成器即可迭代。

为了使类可迭代,必须实现一个名称为 Symbol Symbol.iterator 的方法。该方法必须返回一个具有 next() 方法的迭代器对象。并且 next() 方法必须返回具有 value 属性和或或布尔型 done 属性的迭代结果对象。示例 12-1 实现了一个可迭代的 Range 类,并演示了如何创建可迭代的、迭代器和迭代结果对象。

示例 12-1 一个可迭代数值范围类

/*
 * A Range object represents a range of numbers {x: from <= x <= to}
 * Range defines a has() method for testing whether a given number is a member
 * of the range. Range is iterable and iterates all integers within the range.
 */
class Range {
    constructor (from, to) {
        this.from = from;
        this.to = to;
    }

    // Make a Range act like a Set of numbers
    has(x) { return typeof x === "number" && this.from <= x && x <= this.to; }

    // Return string representation of the range using set notation
    toString() { return `{ x | ${this.from} ≤ x ≤ ${this.to} }`; }

    // Make a Range iterable by returning an iterator object.
    // Note that the name of this method is a special symbol, not a string.
    [Symbol.iterator]() {
        // Each iterator instance must iterate the range independently of
        // others. So we need a state variable to track our location in the
        // iteration. We start at the first integer >= from.
        let next = Math.ceil(this.from);  // This is the next value we return
        let last = this.to;               // We won't return anything > this
        return {                          // This is the iterator object
            // This next() method is what makes this an iterator object.
            // It must return an iterator result object.
            next() {
                return (next <= last)   // If we haven't returned last value yet
                    ? { value: next++ } // return next value and increment it
                    : { done: true };   // otherwise indicate that we're done.
            },

            // As a convenience, we make the iterator itself iterable.
            [Symbol.iterator]() { return this; }
        };
    }
}

for(let x of new Range(1,10)) console.log(x); // Logs numbers 1 to 10
[...new Range(-2,2)]                          // => [-2, -1, 0, 1, 2]

除了使类可迭代外,定义返回可迭代值的函数也非常有用。考虑 JavaScript 数组的 map() 和 filter() 方法的这些基于迭代的替代方法:

// Return an iterable object that iterates the result of applying f()
// to each value from the source iterable
function map(iterable, f) {
    let iterator = iterable[Symbol.iterator]();
    return {     // This object is both iterator and iterable
        [Symbol.iterator]() { return this; },
        next() {
            let v = iterator.next();
            if (v.done) {
                return v;
            } else {
                return { value: f(v.value) };
            }
        }
    };
}

// Map a range of integers to their squares and convert to an array
[...map(new Range(1,4), x => x*x)]  // => [1, 4, 9, 16]

// Return an iterable object that filters the specified iterable,
// iterating only those elements for which the predicate returns true
function filter(iterable, predicate) {
    let iterator = iterable[Symbol.iterator]();
    return { // This object is both iterator and iterable
        [Symbol.iterator]() { return this; },
        next() {
            for(;;) {
                let v = iterator.next();
                if (v.done || predicate(v.value)) {
                    return v;
                }
            }
        }
    };
}

// Filter a range so we're left with only even numbers
[...filter(new Range(1,10), x => x % 2 === 0)]  // => [2,4,6,8,10]

可迭代对象和迭代器的一个关键特性是它们固有的惰性:当需要计算才能计算下一个值时,可以将计算推迟到实际需要该值时进行。例如,假设有一个很长的文本字符串,想将其标记为以空格分隔的单词。可以简单地使用字符串的 split() 方法,但是如果这样做,则必须先处理整个字符串,才能使用第一个单词。最后,将为返回的数组及其中的所有字符串分配大量内存。这是一个允许懒惰地迭代字符串的祖母而无需一次将所有单词都保留在内存中的函数(在 ES2020 中,使用 §11.3.2 中描述的迭代器返回 matchAll() 方法可以更轻松地实现此函数。):

function words(s) {
    var r = /\s+|$/g;                     // Match one or more spaces or end
    r.lastIndex = s.match(/[^ ]/).index;  // Start matching at first nonspace
    return {                              // Return an iterable iterator object
        [Symbol.iterator]() {             // This makes us iterable
            return this;
        },
        next() {                          // This makes us an iterator
            let start = r.lastIndex;      // Resume where the last match ended
            if (start < s.length) {       // If we're not done
                let match = r.exec(s);    // Match the next word boundary
                if (match) {              // If we found one, return the word
                    return { value: s.substring(start, match.index) };
                }
            }
            return { done: true };        // Otherwise, say that we're done
        }
    };
}

[...words(" abc def  ghi! ")] // => ["abc", "def", "ghi!"]

12.2.1 关闭迭代器:Return 方法

想象一下 words() 迭代器的(服务器端)JavaScript 变体,它不用源字符串作为实参,而是使用文件名,打开文件,从文件中读取行,然后从这些行中迭代单词。在大多数操作系统中,打开文件以从其中读取文件的程序需要记住在完成读取后关闭这些文件,因此,这个假设的迭代器将确保在 next() 方法返回文件中的最后一个字之后关闭文件。

但是迭代器并不总是一直运行到最后:for/of 循环可能会因中断或返回或异常而终止。同样,当迭代器用于解构赋值时,仅调用 next() 方法足够多次,以获取每个指定变量的值。迭代器可能有更多可能返回的值,但是它们永远不会被请求。

如果我们假设的文件中单词迭代器从未一直运行到最后,它仍然需要关闭它打开的文件。因此,迭代器对象可以实现 return() 方法与 next() 方法一起使用。如果迭代在 next() 返回完了属性设置为 true 的迭代结果之前停止(通常是因为通过 break 语句提前离开了 for/of 循环),则解释器将检查迭代器对象是否具有 return() 方法。如果存在此方法,则解释器将不带任何参数调用它,从而使迭代器有机会关闭文件,释放内存以及自行清理。return() 方法必须返回迭代器结果对象。对象的属性被忽略,但是返回非对象值是错误的。

for/of 循环和展开运算符是 JavaScript 真正实用功能,因此在创建 API 时,最好在尽可能的使用它们。但是必须使用一个可迭代的对象,其迭代器对象以及迭代器的结果对象,这会使过程变得有些复杂。幸运的是,生成器可以极大地简化自定义迭代器的创建,这将在本章的其余部分中看到。

12.3 生成器

生成器是一种使用强大的新 ES6 语法定义的迭代器;当要迭代的值不是数据结构的元素而是计算结果时,此功能特别有用。

要创建生成器,必须首先定义一个生成器函数。生成器函数在语法上类似于常规 JavaScript 函数,但使用关键字 function* 而不是 function 定义。(从技术上讲,这不是新关键字,只是关键字 function 之后和函数名称之前的 *。)调用生成器函数时,它实际上并不执行函数主体,而是返回生成器对象。该生成器对象是一个迭代器。调用其 next() 方法会使生成器函数的主体从头开始运行(或无论其当前位置是什么),直到到达 yield 语句为止。yield 是 ES6 新特性,类似于 return 语句。yield 语句的值成为迭代器上 next() 调用返回的值。一个示例使这更加清楚:

// A generator function that yields the set of one digit (base-10) primes.
function* oneDigitPrimes() { // Invoking this function does not run the code
    yield 2;                 // but just returns a generator object. Calling
    yield 3;                 // the next() method of that generator runs
    yield 5;                 // the code until a yield statement provides
    yield 7;                 // the return value for the next() method.
}

// When we invoke the generator function, we get a generator
let primes = oneDigitPrimes();

// A generator is an iterator object that iterates the yielded values
primes.next().value          // => 2
primes.next().value          // => 3
primes.next().value          // => 5
primes.next().value          // => 7
primes.next().done           // => true

// Generators have a Symbol.iterator method to make them iterable
primes[Symbol.iterator]()    // => primes

// We can use generators like other iterable types
[...oneDigitPrimes()]        // => [2,3,5,7]
let sum = 0;
for(let prime of oneDigitPrimes()) sum += prime;
sum                          // => 17

在此示例中,我们使用 function* 语句定义了生成器。但是,像常规函数一样,我们也可以在 from 表达式中定义生成器。再一次,我们只在 function 关键字之后加上一个星号:

const seq = function*(from,to) {
    for(let i = from; i <= to; i++) yield i;
};
[...seq(3,5)]  // => [3, 4, 5]

在类和对象文字中,我们在定义方法时可以使用速记标记来完全省略 function 关键字。要在这种情况下定义生成器,我们只需在方法名称之前使用星号即可:

let o = {
    x: 1, y: 2, z: 3,
    // A generator that yields each of the keys of this object
    *g() {
        for(let key of Object.keys(this)) {
            yield key;
        }
    }
};
[...o.g()] // => ["x", "y", "z", "g"]

请注意,无法使用箭头函数语法编写生成器函数。

生成器通常使定义可迭代类特别容易。我们可以用更短的 *[Symbol.iterator];() 生成器函数代替示例 12-1 中的 [Symbol.iterator]() 方法,如下所示:

*[Symbol.iterator]() {
    for(let x = Math.ceil(this.from); x <= this.to; x++) yield x;
}

请参阅第 9 章中的示例 9-3,可以看到在上下文中看到此基于生成器的迭代器函数。

12.3.1 生成器示例

如果生成器实际上通过执行某种计算来生成它们产生的值,则它们会更有趣。例如,这里是生成斐波那契数的生成器函数:

function* fibonacciSequence() {
    let x = 0, y = 1;
    for(;;) {
        yield y;
        [x, y] = [y, x+y];  // Note: destructuring assignment
    }
}

请注意,此处的 fibonacciSequence() 生成器函数具有无限循环,并且永久产生值而不会返回。如果将此生成器与 ... 展开运算符一起使用,它将循环播放直到内存耗尽且程序崩溃。小心地在 for/of 循环中使用它:

// Return the nth Fibonacci number
function fibonacci(n) {
    for(let f of fibonacciSequence()) {
        if (n-- <= 0) return f;
    }
}
fibonacci(20)   // => 10946

这种无限生成器在使用 take() 生成器时变得更加有用,如下所示:

// Yield the first n elements of the specified iterable object
function* take(n, iterable) {
    let it = iterable[Symbol.iterator](); // Get iterator for iterable object
    while(n-- > 0) {           // Loop n times:
        let next = it.next();  // Get the next item from the iterator.
        if (next.done) return; // If there are no more values, return early
        else yield next.value; // otherwise, yield the value
    }
}

// An array of the first 5 Fibonacci numbers
[...take(5, fibonacciSequence())]  // => [1, 1, 2, 3, 5]

这是另一个有用的生成器函数,它交错多个可迭代对象的元素:

// Given an array of iterables, yield their elements in interleaved order.
function* zip(...iterables) {
    // Get an iterator for each iterable
    let iterators = iterables.map(i => i[Symbol.iterator]());
    let index = 0;
    while(iterators.length > 0) {       // While there are still some iterators
        if (index >= iterators.length) {    // If we reached the last iterator
            index = 0;                      // go back to the first one.
        }
        let item = iterators[index].next(); // Get next item from next iterator.
        if (item.done) {                    // If that iterator is done
            iterators.splice(index, 1);     // then remove it from the array.
        }
        else {                              // Otherwise,
            yield item.value;               // yield the iterated value
            index++;                        // and move on to the next iterator.
        }
    }
}

// Interleave three iterable objects
[...zip(oneDigitPrimes(),"ab",[0])]     // => [2,"a",0,3,"b",5,7]

12.3.2 yield* 和递归生成器

除了前面示例中定义的 zip() 生成器之外,具有类似的生成器功能可能会很有用,该功能可以按顺序生成多个可迭代对象的元素,而不是交织它们。我们可以这样编写生成器:

function* sequence(...iterables) {
    for(let iterable of iterables) {
        for(let item of iterable) {
            yield item;
        }
    }
}

[...sequence("abc",oneDigitPrimes())]  // => ["a","b","c",2,3,5,7]

这种生成其他可迭代对象的元素的过程在生成器函数中已经足够普遍,以至于为它 ES6 具有特殊的语法。yield* 关键字类似于 yield,除了它不产生单个值,而是迭代一个可迭代的对象并产生每个结果值。我们使用的 sequence() 生成器函数可以通过 yield* 进行简化,如下所示:

function* sequence(...iterables) {
    for(let iterable of iterables) {
        yield* iterable;
    }
}

[...sequence("abc",oneDigitPrimes())]  // => ["a","b","c",2,3,5,7]

数组 forEach() 方法通常是一种循环遍历数组元素的好方法,因此可能会很想像这样编写 sequence() 函数:

function* sequence(...iterables) {
    iterables.forEach(iterable => yield* iterable );  // Error
}

但是,这不起作用。yield 和 yield* 只能在生成器函数中使用,但是此代码中的嵌套箭头函数是常规函数,而不是 function* 生成器函数,因此不允许 yield。

yield* 可用于任何种类的可迭代对象,包括使用生成器实现的可迭代对象。这意味着 yield* 允许我们定义递归生成器,例如,可以使用此功能在递归定义的树结构上进行简单的非递归迭代。

12.4 高级生成器特性

生成器函数最常见的用途是创建迭代器,但是生成器的基本特性是它们允许我们暂停计算,产生中间结果,然后在以后恢复计算。这意味着生成器具有的功能超出了迭代器的功能,我们将在以下各节中探讨这些功能。

12.4.1 生成器函数的返回值

到目前为止,我们看到的生成器函数还没有 return 语句,或者,如果有的话,它们只是被用来引起较早的返回,而不是产生返回值。但是,像任何函数一样,生成器函数可以返回一个值。为了了解在这种情况下会发生什么,请回忆一下迭代是如何工作的。next() 函数的返回值是一个具有 value 属性和或或 done 属性的对象。对于典型的迭代器和生成器,如果定义了 value 属性,则 done 属性是 undefined 或为 false。如果 done 为 true,那么值就是 undefined。但是,如果生成器返回一个值,则对 next 的最终调用将返回一个同时具有 value ​​和 done 定义的对象。 value 属性保存生成器函数的返回值,并且 done 属性为 true,表示没有更多的值可以迭代。最终值将被 for/of 循环和展开运算符忽略,但可用通过对 next() 的显式调用手动进行迭代:

function *oneAndDone() {
    yield 1;
    return "done";
}

// The return value does not appear in normal iteration.
[...oneAndDone()]   // => [1]

// But it is available if you explicitly call next()
let generator = oneAndDone();
generator.next()           // => { value: 1, done: false}
generator.next()           // => { value: "done", done: true }
// If the generator is already done, the return value is not returned again
generator.next()           // => { value: undefined, done: true }

12.4.2 yield 表达式的值

在前面的讨论中,我们将 yield 视为带有值但没有自身值的语句。但是,实际上,yield 是一个表达式,可以有一个值。

调用生成器的 next() 方法时,生成器函数将运行直至到达 yield 表达式。将评估 yield 关键字之后的表达式,该值将成为 next() 调用的返回值。此时,生成器函数在评估 yield 表达式的中间立即停止执行。下次调用生成器的 next() 方法时,传递给 next() 的参数成为已暂停的 yield 表达式的值。因此,生成器将把 yield 的值返回给它的调用者,然后调用者通过 next() 将值传递给生成器。生成器和调用者是两个独立的执行流,来回传递值(和控制)。以下代码说明:

function* smallNumbers() {
    console.log("next() invoked the first time; argument discarded");
    let y1 = yield 1;    // y1 == "b"
    console.log("next() invoked a second time with argument", y1);
    let y2 = yield 2;    // y2 == "c"
    console.log("next() invoked a third time with argument", y2);
    let y3 = yield 3;    // y3 == "d"
    console.log("next() invoked a fourth time with argument", y3);
    return 4;
}

let g = smallNumbers();
console.log("generator created; no code runs yet");
let n1 = g.next("a");   // n1.value == 1
console.log("generator yielded", n1.value);
let n2 = g.next("b");   // n2.value == 2
console.log("generator yielded", n2.value);
let n3 = g.next("c");   // n3.value == 3
console.log("generator yielded", n3.value);
let n4 = g.next("d");   // n4 == { value: 4, done: true }
console.log("generator returned", n4.value);

此代码运行时,将产生以下输出,演示两个代码块之间的来回交互:

generator created; no code runs yet
next() invoked the first time; argument discarded
generator yielded 1
next() invoked a second time with argument b
generator yielded 2
next() invoked a third time with argument c
generator yielded 3
next() invoked a fourth time with argument d
generator returned 4

注意此代码中的不对称性。next() 的首次调用将启动生成器,但是生成器无法访问传递给该调用的值。

12.4.3 生成器的 return() 和 throw() 方法

我们已经看到可以接收生成器函数产生或返回的值。可以在调用生成器的 next() 方法时将值传递给正在运行的生成器。

除了使用 next() 向生成器提供输入之外,还可以通过调用生成器的 return() 和 throw() 方法来更改生成器内部的控制流。顾名思义,在生成器上调用这些方法会导致其返回值或引发异常,就像生成器中的下一条语句是 return 或 throw 一样。

从本章前面的内容回想起,如果迭代器定义了 return() 方法且迭代提早停止,则解释器将自动调用 return() 方法,以使迭代器有机会关闭文件或进行其他清理。对于生成器,不能定义自定义的 return() 方法来处理清理,但是可以构造生成器代码以使用 try/finally 语句,以确保生成器返回时执行清理操作(在 finally 块中)。通过强制生成器返回,生成器的内置 return() 方法可确保在不再使用生成器时运行清除代码。

正如生成器的 next() 方法允许我们将任意值传递给正在运行的生成器一样,生成器的 throw() 方法为我们提供了一种将任意信号(以异常形式)发送到生成器的方法。调用 throw() 方法总是会在生成器内部引起异常。但是,如果生成器函数有适当的异常处理代码编,则该异常是致命的,不过这可以用作更改生成器行为的一种手段。例如,想象一下产生一个不断增加的整数序列的计数器生成器。可以这样编写,使用 throw() 发送的异常将计数器重置为零。

当生成器使用 yield* 从其他可迭代对象生成值时,对生成器的 next() 方法的调用会导致对可迭代对象的 next() 方法的调用。return() 和 throw() 方法也是如此。 如果生成器在定义了这些方法的可迭代对象上使用 yield*,则在生成器上调用 return() 或 throw() 会导致依次调用迭代器的 return() 或 throw() 方法。所有迭代器都必须具有 next() 方法。需要在不完整的迭代后进行清理的迭代器应定义一个 return() 方法。而且,任何迭代器都可以定义 throw() 方法,尽管我不知道有任何实际原因。

12.4.4 关于生成器的最后一个注意点

生成器是一个非常强大的通用控制结构。它们使我们能够使用 yield 暂停计算,并在以后任意任意时间使用任意输入值重新开始计算。可以使用生成器在单线程 JavaScript 代码中创建一种协作线程系统。而且,即使某些函数调用实际上是异步的并且依赖于网络事件,也可以使用生成器来掩盖程序的异步部分,从而使代码显得顺序和同步。

尝试使用生成器执行这些操作会导致代码难以理解或解释。但是,它已经成为了过去时,唯一真正实用的用例是管理异步代码。为此,JavaScript 现在具有 async 和 await 关键字(请参阅第 13 章),并且不再有任何理由以这种方式滥用生成器。

12.5 总结

在本章中,您学习了:

for/of 循环和 ... 展开运算符可迭代对象。

如果对象具有符号名称为 [Symbol.iterator] 的方法,则该方法返回迭代器对象,该对象是可迭代的。

迭代器对象具有 next() 方法,该方法返回迭代结果对象。

迭代结果对象具有一个 value 属性,该属性保存下一个迭代值(如果有)。如果迭代已完成,则结果对象必须将 done 属性设置为 true。

可以通过定义返回对象的 Symbol.iterator 方法和返回迭代结果对象的 next() 方法来实现自己的可迭代对象。还可以实现接受迭代器参数并返回迭代器值的函数。

生成器函数(用 function* 代替 function 定义的函数)是定义迭代器的另一种方法。

当调用生成器函数时,该函数的主体不会立即运行。相反,返回值是一个可迭代的迭代器对象。每次调用迭代器的 next() 方法时,都会运行另一部分生成器函数。

生成器函数可以使用 yield 运算符来指定迭代器返回的值。每次调用 next() 都会使生成器函数运行到下一个 yield 表达式。然后,该 yield 表达式的值将成为迭代器返回的值。当没有更多的 yield 表达式时,生成器函数将返回,并且迭代完成。