## 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()`完全讀完。
 **練習:將正常功能轉換為發生器**
`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']);
```
 **練習:過濾迭代**
`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'
```
 **練習:迭代嵌套數組**
`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.其余章節在哪里?