💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
## 35 同步生成器(高级) > 原文: [http://exploringjs.com/impatient-js/ch_sync-generators.html](http://exploringjs.com/impatient-js/ch_sync-generators.html) > > 贡献者:[steve000](https://github.com/steve000) ### 35.1 什么是同步生成器? 同步生成器是函数定义和方法定义的特殊版本,它们始终返回同步可迭代: ```js // 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`和星号的组合。 * 方法:`*`是一个修饰符(类似于`static`和`get`)。 #### 35.1.1 生成器函数返回 iterables 并通过`yield`填充它们 如果调用生成器函数,它将返回一个 iterable(实际上:也是可迭代的迭代器)。生成器通过`yield`运算符填充可迭代的: ```js 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' ``` #### 35.1.2 `yield`暂停生成器功能 到目前为止,`yield`看起来像是一种向迭代中添加值的简单方法。但是,它做的远不止于此 - 它还会暂停和退出生成器功能: * 与`return`类似,`yield`退出函数体。 * 与`return`不同,如果再次调用该函数,则会在`yield`之后直接执行。 让我们通过以下生成器函数来检查它的含义。 ```js let location = 0; function* genFunc2() { location = 1; yield 'a'; location = 2; yield 'b'; location = 3; } ``` 生成器函数的结果称为 _ 生成器对象 _。它不仅仅是一个可迭代的,但这超出了本书的范围(如果您对更多细节感兴趣,请参考[“探索 ES6”](http://exploringjs.com/es6/ch_generators.html))。 为了使用`genFunc2()`,我们必须首先创建生成器对象`genObj`。 `genFunc2()`现在暂停“身体前”。 ```js const genObj = genFunc2(); // genFunc2() is now paused “before” its body: assert.equal(location, 0); ``` `genObj`实现[迭代协议](ch_sync-iteration.html)。因此,我们通过`genObj.next()`控制`genFunc2()`的执行。调用该方法,恢复暂停的`genFunc2()`并执行它直到有`yield`。然后执行暂停,`.next()`返回`yield`的操作数: ```js assert.deepEqual( genObj.next(), {value: 'a', done: false}); // genFunc2() is now paused directly after the first `yield`: assert.equal(location, 1); ``` 请注意,产生的值`'a'`包装在一个对象中,这就是迭代总是传递它们的值的方式。 我们再次调用`genObj.next()`并继续执行我们先前暂停的位置。一旦遇到第二个`yield`,`genFunc2()`暂停,`.next()`返回产生的值`'b'`。 ```js assert.deepEqual( genObj.next(), {value: 'b', done: false}); // genFunc2() is now paused directly after the second `yield`: assert.equal(location, 2); ``` 我们再一次调用`genObj.next()`并继续执行直到它离开`genFunc2()`的主体: ```js genObj.next(), {value: undefined, done: true}); // We have reached the end of genFunc2(): assert.equal(location, 3); ``` 这次,`.next()`的结果的属性`.done`是`true`,这意味着可迭代完成。 #### 35.1.3 为什么`yield`暂停执行? `yield`暂停执行有什么好处?为什么它不像 Array 方法`.push()`那样工作并用值填充 iterable - 没有暂停? 由于暂停,生成器提供 _ 协同程序 _ 的许多功能(认为协同多任务的进程)。例如,当你要求迭代的下一个值时,该值被计算 _ 懒惰 _(按需)。以下两个生成器函数演示了这意味着什么。 ```js /** * 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`: ```js 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()`完全读完。 ![](https://img.kancloud.cn/3e/d5/3ed5755d562179ae6c199264f5e21157.svg) **练习:将正常功能转换为发生器** `exercises/generators/fib_seq_test.js` #### 35.1.4 示例:迭代迭代 以下函数`mapIter()`类似于`Array.from()`,但它返回一个可迭代的,而不是一个数组,并根据需要生成其结果。 ```js 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']); ``` ![](https://img.kancloud.cn/3e/d5/3ed5755d562179ae6c199264f5e21157.svg) **练习:过滤迭代** `exercises/generators/filter_iter_gen_test.js` ### 35.2 从生成器调用生成器(高级) #### 35.2.1 通过`yield*`调用生成器 `yield`只能直接在生成器中工作 - 到目前为止,我们还没有看到将屈服委托给另一个函数或方法的方法。 让我们首先检查 _ 不是 _ 的工作原理:在下面的例子中,我们希望`foo()`调用`bar()`,以便后者为前者产生两个值。唉,天真的方法失败了: ```js function* foo() { // Nothing happens if we call `bar()`: bar(); } function* bar() { yield 'a'; yield 'b'; } assert.deepEqual( [...foo()], []); ``` 为什么这不起作用?函数调用`bar()`返回一个我们忽略的 iterable。 我们想要的是`foo()`产生`bar()`产生的所有东西。这就是`yield*`运算符的作用: ```js function* foo() { yield* bar(); } function* bar() { yield 'a'; yield 'b'; } assert.deepEqual( [...foo()], ['a', 'b']); ``` 换句话说,之前的`foo()`大致相当于: ```js function* foo() { for (const x of bar()) { yield x; } } ``` 请注意,`yield*`适用于任何可迭代: ```js function* gen() { yield* [1, 2]; } assert.deepEqual( [...gen()], [1, 2]); ``` #### 35.2.2 示例:在树上迭代 `yield*`允许我们在生成器中进行递归调用,这在迭代递归数据结构(如树)时很有用。举例来说,二叉树的数据结构如下。 ```js 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`的实例: ```js 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' ``` ![](https://img.kancloud.cn/3e/d5/3ed5755d562179ae6c199264f5e21157.svg) **练习:迭代嵌套数组** `exercises/generators/iter_nested_arrays_test.js` ### 35.3 示例:重用循环 生成器的一个重要用例是提取和重用循环功能。 #### 35.3.1 要重用的循环 作为一个例子,考虑以下函数迭代文件树并记录它们的路径(它使用 [Node.js API](https://nodejs.org/en/docs/) 这样做): ```js 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); ``` 我们如何重用这个循环来做除记录路径之外的其他事情? #### 35.3.2 内部迭代(推送) 重用迭代代码的一种方法是通过 [_ 内部迭代 _](ch_arrays.html#external-iteration-internal-iteration) :将每个迭代值传递给回调(A 行)。 ```js 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)); ``` #### 35.3.3 外部迭代(拉) 另一种重用迭代代码的方法是通过 [_ 外部迭代 _](ch_arrays.html#external-iteration-internal-iteration) :我们可以编写一个生成所有迭代值的生成器。 ```js 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)]; ``` ### 35.4 生成器的高级功能 * `yield`也可以通过`.next()` _ 接收 _ 数据。有关详细信息,请参阅[“探索 ES6”](http://exploringjs.com/es6/ch_generators.html#sec_generators-as-observers)。 * `yield*`的结果是其操作数返回的结果。有关详细信息,请参阅[“探索 ES6”](http://exploringjs.com/es6/ch_generators.html#_recursion-via-yield)。