## 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)。
- I.背景
- 1.关于本书(ES2019 版)
- 2.常见问题:本书
- 3. JavaScript 的历史和演变
- 4.常见问题:JavaScript
- II.第一步
- 5.概览
- 6.语法
- 7.在控制台上打印信息(console.*)
- 8.断言 API
- 9.测验和练习入门
- III.变量和值
- 10.变量和赋值
- 11.值
- 12.运算符
- IV.原始值
- 13.非值undefined和null
- 14.布尔值
- 15.数字
- 16. Math
- 17. Unicode - 简要介绍(高级)
- 18.字符串
- 19.使用模板字面值和标记模板
- 20.符号
- V.控制流和数据流
- 21.控制流语句
- 22.异常处理
- 23.可调用值
- VI.模块化
- 24.模块
- 25.单个对象
- 26.原型链和类
- 七.集合
- 27.同步迭代
- 28.数组(Array)
- 29.类型化数组:处理二进制数据(高级)
- 30.映射(Map)
- 31. WeakMaps(WeakMap)
- 32.集(Set)
- 33. WeakSets(WeakSet)
- 34.解构
- 35.同步生成器(高级)
- 八.异步
- 36. JavaScript 中的异步编程
- 37.异步编程的 Promise
- 38.异步函数
- IX.更多标准库
- 39.正则表达式(RegExp)
- 40.日期(Date)
- 41.创建和解析 JSON(JSON)
- 42.其余章节在哪里?