33. 同步生成器(高级)

33.1 什么是同步生成器?

同步生成器是函数定义和方法定义的特殊版本,它们始终返回同步可迭代:

// 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() {
    // ···
  }
}

星号(*)将函数和方法标记为生成器:

  • 功能:伪关键字function*是关键字function和星号的组合。
  • 方法:*是一个修饰符(类似于staticget)。

33.1.1 生成器函数返回 iterables 并通过yield填充它们

如果调用生成器函数,它将返回一个 iterable(实际上:也是可迭代的迭代器)。生成器通过yield运算符填充可迭代的:

function* genFunc1() {
  yield 'a';
  yield 'b';
}
const iterable = genFunc1();
// Convert the iterable to an Array, to check what’s inside:
assert.deepEqual([...iterable], ['a', 'b']);
// You can also use a for-of loop
for (const x of genFunc1()) {
  console.log(x);
}
// Output:
// 'a'
// 'b'

33.1.2 yield暂停生成器功能

到目前为止,yield看起来像是一种向迭代中添加值的简单方法。但是,它做的远不止于此 - 它还会暂停和退出生成器功能:

  • return类似,yield退出函数体。
  • return不同,如果再次调用该函数,则会在yield之后直接执行。

让我们通过以下生成器函数来检查它的含义。

let location = 0;
function* genFunc2() {
  location = 1;
  yield 'a';
  location = 2;
  yield 'b';  
  location = 3;
}

生成器函数的结果称为 _ 生成器对象 _。它不仅仅是一个可迭代的,但这超出了本书的范围。

为了使用genFunc2(),我们必须首先创建生成器对象genObjgenFunc2()现在暂停“身体前”。

const genObj = genFunc2();
// genFunc2() is now paused “before” its body:
assert.equal(location, 0);

genObj实现迭代协议。因此,我们通过genObj.next()控制genFunc2()的执行。调用该方法,恢复暂停的genFunc2()并执行它直到有yield。然后执行暂停,.next()返回yield的操作数:

assert.deepEqual(
  genObj.next(), {value: 'a', done: false});
// genFunc2() is now paused directly after the first `yield`:
assert.equal(location, 1);

请注意,产生的值'a'包装在一个对象中,这就是迭代总是传递它们的值的方式。

我们再次调用genObj.next()并继续执行我们先前暂停的位置。一旦遇到第二个yieldgenFunc2()暂停,.next()返回产生的值'b'

assert.deepEqual(
  genObj.next(), {value: 'b', done: false});
// genFunc2() is now paused directly after the second `yield`:
assert.equal(location, 2);

我们再一次调用genObj.next()并继续执行直到它离开genFunc2()的主体:

  genObj.next(), {value: undefined, done: true});
// We have reached the end of genFunc2():
assert.equal(location, 3);

这次,.next()的结果的属性.donetrue,这意味着可迭代完成。

33.1.3 为什么yield暂停执行?

yield暂停执行有什么好处?为什么它不像 Array 方法.push()那样工作并用值填充 iterable - 没有暂停?

由于暂停,生成器提供 _ 协同程序 _ 的许多功能(认为协同多任务的进程)。例如,当你要求迭代的下一个值时,该值被计算 _ 懒惰 _(按需)。以下两个生成器函数演示了这意味着什么。

/**
 * 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());
assert.deepEqual(
  numberedLines.next(), {value: '1: A line', done: false});
assert.deepEqual(
  numberedLines.next(), {value: '2: Another line', done: false});

每次我们通过.next()numberedLines询问另一个值时,numberLines()只询问genLines()一行并给它编号。如果genLines()同步从大文件中读取它的行,我们将能够从文件中读取第一个编号行。如果yield没有暂停,我们必须等到genLines()完全读完。

练习:将正常功能转换为发生器

exercises/generators/fib_seq_test.js

33.1.4 示例:迭代迭代

以下函数mapIter()类似于Array.from(),但它返回一个可迭代的,而不是一个数组,并根据需要生成其结果。

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);
assert.deepEqual([...iterable], ['aa', 'bb']);

练习:过滤迭代

exercises/generators/filter_iter_gen_test.js

33.2 从生成器调用生成器(高级)

33.2.1 通过yield*调用生成器

yield只能直接在生成器中工作 - 到目前为止,我们还没有看到将屈服委托给另一个函数或方法的方法。

让我们首先检查 _ 不是 _ 的工作原理:在下面的例子中,我们希望foo()调用bar(),以便后者为前者产生两个值。唉,天真的方法失败了:

function* foo() {
  // Nothing happens if we call `bar()`:
  bar();
}
function* bar() {
  yield 'a';
  yield 'b';
}
assert.deepEqual(
  [...foo()], []);

为什么这不起作用?函数调用bar()返回一个我们忽略的 iterable。

我们想要的是foo()产生bar()产生的所有东西。这就是yield*运算符的作用:

function* foo() {
  yield* bar();
}
function* bar() {
  yield 'a';
  yield 'b';
}
assert.deepEqual(
  [...foo()], ['a', 'b']);

换句话说,之前的foo()大致相当于:

function* foo() {
  for (const x of bar()) {
    yield x;
  }
}

请注意,yield*适用于任何可迭代:

function* gen() {
  yield* [1, 2];
}
assert.deepEqual(
  [...gen()], [1, 2]);

33.2.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/generators/iter_nested_arrays_test.js

33.3 示例:重用循环

生成器的一个重要用例是提取和重用循环功能。

33.3.1 要重用的循环

作为一个例子,考虑以下函数迭代文件树并记录它们的路径(它使用 Node.js API 这样做):

function logFiles(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()) {
      logFiles(filePath); // recursive call
    }
  }
}
const rootDir = process.argv[2];
logFiles(rootDir);

我们如何重用这个循环来做除记录路径之外的其他事情?

33.3.2 内部迭代(推送)

重用迭代代码的一种方法是通过 内部迭代 :将每个迭代值传递给回调(A 行)。

function iterFiles(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()) {
      iterFiles(filePath, callback);
    }
  }
}
const rootDir = process.argv[2];
const paths = [];
iterFiles(rootDir, p => paths.push(p));

33.3.3 外部迭代(拉)

另一种重用迭代代码的方法是通过 外部迭代 :我们可以编写一个生成所有迭代值的生成器。

function* iterFiles(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* iterFiles(filePath);
    }
  }
}
const rootDir = process.argv[2];
const paths = [...iterFiles(rootDir)];

33.4 生成器的高级功能

  • yield也可以通过.next() _ 接收 _ 数据。
  • yield*的结果是其操作数返回的结果。
下一节:本章介绍了 JavaScript 中异步编程的基础。