同步生成器是函数定义和方法定义的特殊版本,它们总是返回同步可迭代对象
// Generator function declaration
function* genFunc1() { /*···*/ }
// Generator function expression
const genFunc2 = function* () { /*···*/ };
// Generator method definition in an object literal
const obj = {
* generatorMethod() {
// ···
};
}
// Generator method definition in a class definition
// (class declaration or class expression)
class MyClass {
* generatorMethod() {
// ···
} }
星号 (`*`) 将函数和方法标记为生成器
如果我们调用一个生成器函数,它会返回一个可迭代对象(实际上是一个也是可迭代的迭代器)。生成器通过 `yield` 运算符填充该可迭代对象
function* genFunc1() {
yield 'a';
yield 'b';
}
const iterable = genFunc1();
// Convert the iterable to an Array, to check what’s inside:
.deepEqual(
assertArray.from(iterable), ['a', 'b']
;
)
// We can also use a for-of loop
for (const x of genFunc1()) {
console.log(x);
}// Output:
// 'a'
// 'b'
使用生成器函数涉及以下步骤
因此,`yield` 不仅仅是向可迭代对象添加值——它还会暂停并退出生成器函数
让我们通过以下生成器函数来检查这意味着什么。
let location = 0;
function* genFunc2() {
= 1; yield 'a';
location = 2; yield 'b';
location = 3;
location }
为了使用 `genFunc2()`,我们必须首先创建迭代器/可迭代对象 `iter`。`genFunc2()` 现在在其主体“之前”暂停。
const iter = genFunc2();
// genFunc2() is now paused “before” its body:
.equal(location, 0); assert
`iter` 实现了迭代协议。因此,我们通过 `iter.next()` 控制 `genFunc2()` 的执行。调用该方法会恢复暂停的 `genFunc2()` 并执行它,直到遇到 `yield`。然后执行暂停,`.next()` 返回 `yield` 的操作数
.deepEqual(
assert.next(), {value: 'a', done: false});
iter// genFunc2() is now paused directly after the first `yield`:
.equal(location, 1); assert
请注意,产生的值 `'a'` 被包装在一个对象中,这就是迭代器始终传递值的方式。
我们再次调用 `iter.next()`,执行从我们之前暂停的地方继续。一旦我们遇到第二个 `yield`,`genFunc2()` 就会暂停,`.next()` 返回产生的值 `'b'`。
.deepEqual(
assert.next(), {value: 'b', done: false});
iter// genFunc2() is now paused directly after the second `yield`:
.equal(location, 2); assert
我们再调用一次 `iter.next()`,执行继续,直到它离开 `genFunc2()` 的主体
.deepEqual(
assert.next(), {value: undefined, done: true});
iter// We have reached the end of genFunc2():
.equal(location, 3); assert
这一次,`.next()` 结果的属性 `.done` 为 `true`,这意味着迭代器已完成。
`yield` 暂停执行有什么好处?为什么它不像数组方法 `.push()` 那样简单地用值填充可迭代对象而不暂停呢?
由于暂停,生成器提供了许多*协程*的功能(想想协作多任务处理的进程)。例如,当我们请求可迭代对象的下一个值时,该值是*延迟*计算的(按需)。以下两个生成器函数演示了这意味着什么。
/**
* Returns an iterable over lines
*/
function* genLines() {
yield 'A line';
yield 'Another line';
yield 'Last line';
}
/**
* Input: iterable over lines
* Output: iterable over numbered lines
*/
function* numberLines(lineIterable) {
let lineNumber = 1;
for (const line of lineIterable) { // input
yield lineNumber + ': ' + line; // output
++;
lineNumber
} }
请注意,`numberLines()` 中的 `yield` 出现在 `for-of` 循环内。`yield` 可以用在循环内,但不能用在回调函数内(稍后会详细介绍)。
让我们结合两个生成器来生成可迭代对象 `numberedLines`
const numberedLines = numberLines(genLines());
.deepEqual(
assert.next(), {value: '1: A line', done: false});
numberedLines.deepEqual(
assert.next(), {value: '2: Another line', done: false}); numberedLines
这里使用生成器的关键好处是,一切都以增量方式工作:通过 `numberedLines.next()`,我们只向 `numberLines()` 请求一个带编号的行。反过来,它只向 `genLines()` 请求一个不带编号的行。
例如,如果 `genLines()` 从一个大型文本文件中读取行,这种增量性将继续起作用:如果我们向 `numberLines()` 请求一个带编号的行,一旦 `genLines()` 从文本文件中读取了第一行,我们就会得到一个。
如果没有生成器,`genLines()` 将首先读取所有行并返回它们。然后 `numberLines()` 将对所有行进行编号并返回它们。因此,我们必须等待更长的时间才能获得第一个带编号的行。
练习:将普通函数转换为生成器
exercises/sync-generators/fib_seq_test.mjs
以下函数 `mapIter()` 类似于数组方法 `.map()`,但它返回一个可迭代对象,而不是一个数组,并且按需生成结果。
function* mapIter(iterable, func) {
let index = 0;
for (const x of iterable) {
yield func(x, index);
++;
index
}
}
const iterable = mapIter(['a', 'b'], x => x + x);
.deepEqual(
assertArray.from(iterable), ['aa', 'bb']
; )
练习:过滤可迭代对象
exercises/sync-generators/filter_iter_gen_test.mjs
`yield` 只能直接在生成器内部工作——到目前为止,我们还没有看到将 `yield` 委托给另一个函数或方法的方法。
让我们首先看看什么*不起作用*:在下面的例子中,我们希望 `foo()` 调用 `bar()`,以便后者为前者产生两个值。唉,一个天真的方法失败了
function* bar() {
yield 'a';
yield 'b';
}function* foo() {
// Nothing happens if we call `bar()`:
bar();
}.deepEqual(
assertArray.from(foo()), []
; )
为什么这不起作用?函数调用 `bar()` 返回一个可迭代对象,我们忽略了它。
我们希望的是 `foo()` 产生 `bar()` 产生的所有内容。这就是 `yield*` 运算符的作用
function* bar() {
yield 'a';
yield 'b';
}function* foo() {
yield* bar();
}.deepEqual(
assertArray.from(foo()), ['a', 'b']
; )
换句话说,前面的 `foo()` 大致相当于
function* foo() {
for (const x of bar()) {
yield x;
} }
请注意,`yield*` 适用于任何可迭代对象
function* gen() {
yield* [1, 2];
}.deepEqual(
assertArray.from(gen()), [1, 2]
; )
`yield*` 允许我们在生成器中进行递归调用,这在迭代树等递归数据结构时非常有用。以以下二叉树的数据结构为例。
class BinaryTree {
constructor(value, left=null, right=null) {
this.value = value;
this.left = left;
this.right = right;
}
/** Prefix iteration: parent before children */
* [Symbol.iterator]() {
yield this.value;
if (this.left) {
// Same as yield* this.left[Symbol.iterator]()
yield* this.left;
}if (this.right) {
yield* this.right;
}
} }
方法 `[Symbol.iterator]()` 添加了对迭代协议的支持,这意味着我们可以使用 `for-of` 循环来迭代 `BinaryTree` 的实例
const tree = new BinaryTree('a',
new BinaryTree('b',
new BinaryTree('c'),
new BinaryTree('d')),
new BinaryTree('e'));
for (const x of tree) {
console.log(x);
}// Output:
// 'a'
// 'b'
// 'c'
// 'd'
// 'e'
练习:迭代嵌套数组
exercises/sync-generators/iter_nested_arrays_test.mjs
为了准备下一节,我们需要了解两种不同的迭代对象“内部”值的风格
外部迭代(拉取):您的代码通过迭代协议向对象请求值。例如,`for-of` 循环基于 JavaScript 的迭代协议
for (const x of ['a', 'b']) {
console.log(x);
}// Output:
// 'a'
// 'b'
内部迭代(推送):我们将一个回调函数传递给对象的一个方法,该方法将值提供给回调函数。例如,数组有方法 `.forEach()`
'a', 'b'].forEach((x) => {
[console.log(x);
;
})// Output:
// 'a'
// 'b'
下一节将提供两种迭代风格的示例。
生成器的一个重要用例是提取和重用遍历。
例如,考虑以下遍历文件树并记录其路径的函数(它使用Node.js API来实现)
function logPaths(dir) {
for (const fileName of fs.readdirSync(dir)) {
const filePath = path.resolve(dir, fileName);
console.log(filePath);
const stats = fs.statSync(filePath);
if (stats.isDirectory()) {
logPaths(filePath); // recursive call
}
} }
考虑以下目录
mydir/
a.txt
b.txt
subdir/
c.txt
让我们记录 `mydir/` 内的路径
logPaths('mydir');
// Output:
// 'mydir/a.txt'
// 'mydir/b.txt'
// 'mydir/subdir'
// 'mydir/subdir/c.txt'
我们如何重用此遍历并执行除记录路径之外的其他操作?
重用遍历代码的一种方法是通过*内部迭代*:每个遍历的值都被传递给一个回调函数(A 行)。
function visitPaths(dir, callback) {
for (const fileName of fs.readdirSync(dir)) {
const filePath = path.resolve(dir, fileName);
callback(filePath); // (A)
const stats = fs.statSync(filePath);
if (stats.isDirectory()) {
visitPaths(filePath, callback);
}
}
}const paths = [];
visitPaths('mydir', p => paths.push(p));
.deepEqual(
assert,
paths
['mydir/a.txt',
'mydir/b.txt',
'mydir/subdir',
'mydir/subdir/c.txt',
; ])
重用遍历代码的另一种方法是通过*外部迭代*:我们可以编写一个生成器来产生所有遍历的值(A 行)。
function* iterPaths(dir) {
for (const fileName of fs.readdirSync(dir)) {
const filePath = path.resolve(dir, fileName);
yield filePath; // (A)
const stats = fs.statSync(filePath);
if (stats.isDirectory()) {
yield* iterPaths(filePath);
}
}
}const paths = Array.from(iterPaths('mydir'));
《探索 ES6》中关于生成器的章节涵盖了本书范围之外的两个功能