yield
填入yield
暫停產生器函式yield
會暫停執行?yield*
呼叫產生器同步產生器是函式定義和方法定義的特殊版本,它們總是傳回同步可迭代物件
// 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
)。yield
填入如果我們呼叫產生器函式,它會傳回一個可迭代物件(實際上,它是一個也是可迭代物件的迭代器)。產生器會透過 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
暫停產生器函式使用產生器函式涉及以下步驟
iter
(它也是一個可迭代物件)。iter
進行迭代會重複呼叫 iter.next()
。每次,我們都會跳到產生器函式的本體,直到出現傳回值的 yield
。因此,yield
不僅會將值新增到可迭代物件,還會暫停並退出產生器函式
return
類似,yield
會退出函式的本體並傳回一個值(到/透過 .next()
)。return
不同,如果我們重複呼叫(.next()
),執行會直接在 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
會暫停執行?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 中關於產生器的章節涵蓋了兩個超出本書範圍的功能
yield
也可以透過 .next()
的引數接收資料。return
值(不只是 yield
它們)。這些值不會變成迭代值,但可以透過 yield*
擷取。