產生器(Generator)
29 Jan 2020一般的 function 在呼叫後只會回傳一個值或不回傳任何東西,但產生器(generator)卻可以一個接著一個的回傳(yield)多個值;產生器和迭代器常用來一起處理資料流。
Generator Function
function*
是建立產生器的語法,稱為 generator function。
範例如下,定義一個產生器 generator。
function* generator() {
yield 1;
yield 2;
yield 3;
return 123;
}
當呼叫這個產生器時,它不會執行這個 function,而是會回傳一個產生器物件(generator object)來控制這個 function 的執行,或說是迭代器也可以,如下例的 gen 就是一個產生器物件。
const gen = generator();
console.log(gen); // [object GeneratorFunction]{}
接著,使用 next 方法來執行其中的 yield 陳述句,它會跑到下一個 yield 前停止。執行 next 方法會得到一個物件 {done: Boolean, value: any}
,其中 value 為 yield 的回傳值,done 表示是否執行完畢,若為 true 則是執行結束了。
如下,one、two、three 得到的物件皆表示尚未結束,並且回傳其值;oneTwoThree 的 done 為 true 則是結束。
const one = gen.next();
const two = gen.next();
const three = gen.next();
const oneTwoThree = gen.next();
console.log(one); // Object { done: false, value: 1 }
console.log(two); // Object { done: false, value: 2 }
console.log(three); // Object { done: false, value: 3 }
console.log(oneTwoThree); // Object { done: true, value: 123 }
點此 看 demo。
似曾相識?迭代器的寫法如下,只是產生器似乎來得更精簡。
const range = {
start: 1,
end: 3,
[Symbol.iterator]() {
this.current = this.start;
return this;
},
next() {
if (this.current >= 1 && this.current <= 3) {
return { done: false, value: this.current++ };
}
return { done: true, value: 123 };
},
};
const iterator = range[Symbol.iterator]();
const one = iterator.next(); // {done: false, value: 1}
const two = iterator.next(); // {done: false, value: 2}
const three = iterator.next(); // {done: false, value: 3}
const oneTwoThree = iterator.next(); // {done: true, value: 123}
產生器是可迭代的
從上面的例子可得知,產生器是可迭代的。
for (const value of generator()) {
console.log(value); // 依序印出 1, 2, 3
}
console.log(...generator()); // 1 2 3
用產生器改寫迭代器
用產生器來改寫迭代器吧。
由於 range[Symbol.iterator]()
改為回傳一個產生器,因此就不用之前的 next 方法,並能回傳符合規格的物件 {done: Boolean, value: any}
。
const range = {
start: 1,
end: 3,
*[Symbol.iterator]() {
// [Symbol.iterator]: function*() 的縮寫
for (let value = this.start; value <= this.end; value++) {
yield value;
}
},
};
for (const value of range) {
console.log(value); // 依序印出 1, 2, 3
}
console.log(...range); // 依序印出 1, 2, 3
點此 看 demo。
比較起來,產生器的寫法似乎比迭代器來得更精簡。
yield*
委派
yield*
委派(yield*
delegate)是指將迭代的執行交給接在後面的產生器或可迭代的物件。
如下例,產生器物件 gen 每次恢復執行,就會迭代陣列中的一個元素。
const generator = function*() {
yield* ['apple', 'boy', 'cat'];
};
const gen = generator();
因此,依序得到 value 為 apple、boy、cat。
gen.next(); // {value: "apple", done: false}
gen.next(); // {value: "boy", done: false}
gen.next(); // {value: "cat", done: false}
gen.next(); // {value: undefined, done: true}
來看一個更複雜的例子,我們利用產生器委派迭代任務給另一個產生器,這麼做的好處是不需要額外的變數(也就是記憶體)來暫存中間產物。如下,產生器 generateSequeatialNumbers 根據傳入的 start 與 from 參數,來決定要回傳(yield)的連續數值的範圍;產生器 generatePasswords 會多次將迭代交給另一個產生器 generateSequeatialNumbers。
function* generateSequeatialNumbers(start, end) {
for (let i = start; i <= end; i++) yield i;
}
function* generatePasswords() {
// 0~9
yield* generateSequeatialNumbers(48, 57);
// A~Z
yield* generateSequeatialNumbers(65, 90);
// a~z
yield* generateSequeatialNumbers(97, 122);
}
因此,當使用 for...of
loop 來迭代執行 generatePasswords 所得到的產生器物件時,會依序迭代 generateSequeatialNumbers 三次,每次因傳入參數 start 與 end 的不同而有不同的結果。
let str = '';
for (const code of generatePasswords()) {
str += String.fromCharCode(code);
}
console.log(str); // "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
String.fromCharCode 會將傳入的 Unicode 轉為對應的字串。
點此 看 demo。
使用 next/yield
來改變產生器的結果
每一次由 next 開始執行,直到遇到 yield 而暫停。若 next 沒有傳入值,yield 就將其後的運算結果回傳出去;若 next 有傳入值,則捨棄先前 yield 的計算結果,由 next 傳入的值取代。由此可知,yield 幾乎等於賦值(assign,=
)的功用。
這裡有一個產生器 foo,內含一陣列,陣列的元素由 yield 運算後產生,以下用 step 來表示進行的順序。
function* foo() {
const arr = [yield 1, yield 2, yield 3];
console.log(arr, yield 4); // step 5: console.log(...) 得到 [undefined, undefined, undefined] undefined
}
const it = foo();
console.log(it.next().value); // step 1: 得到 1
console.log(it.next().value); // step 2: 得到 2
console.log(it.next().value); // step 3: 得到 3
console.log(it.next().value); // step 4: 得到 4
console.log(it.next().done); // step 6: 得到 true
執行結果
1
2
3
4
[undefined, undefined, undefined] undefined
true
點此 看 demo。
若 next 有傳入值,會怎麼樣呢?就捨棄先前 yield 後方計算的結果,由下一次 next 傳入的值取代,因此,下方的執行結果一樣都會得到 1、2、3、4,但陣列 array 的內容被傳入的取代了。
function* foo() {
const arr = [yield 1, yield 2, yield 3];
console.log(arr, yield 4); // step 5: console.log(...) 得到 [200, 300, 400] 500
}
const it = foo();
console.log(it.next(100).value); // step 1: 得到 1
console.log(it.next(200).value); // step 2: 得到 2,並用 200 取代 yield 1 的結果
console.log(it.next(300).value); // step 3: 得到 3,並用 300 取代 yield 2 的結果
console.log(it.next(400).value); // step 4: 得到 4,並用 400 取代 yield 3 的結果
console.log(it.next(500).done); // step 6: 得到 true,並用 500 取代 yield 4 的結果
執行結果
1
2
3
4
[200, 300, 400] 500
true
點此 看 demo。
拋出錯誤
若想在產生器內拋出錯誤,使用 generator.throw(err)
即可。
function* generator() {
try {
const result = yield 'Hello World'; // (3)
console.log('The execution does not reach here, because the exception is thrown above'); // (4)
} catch (e) {
console.log(e.message); // (5) 顯示錯誤訊息
}
}
const gen = generator();
const result = gen.next().value; // (1)
console.log(result); // Hello World
gen.throw(new Error('Here is an error :->')); // (2)
點此 看 demo。
說明
- (1)
gen.next().value
會執行 (3)yield 'Hello World'
並回傳這個結果,此時產生器暫停,因此可在 (1) 處得到 result 為 Hello World。 - (2)
gen.throw
讓產生器解除暫停,繼續運作,而拋出錯誤Here is an error :->
傳進產生器,產生器在try...catch
區塊捕捉到這個錯誤,因此跳過 (4),進入 (5) 顯示錯誤訊息。
若沒有 try...catch
區塊來做錯誤捕捉,則程式碼會出錯 Uncaught Error: Here is an error :->
。
function* generator() {
const result = yield 'Hello World';
}
const gen = generator();
const result = gen.next().value;
gen.throw(new Error('Here is an error :->'));
得到
Uncaught Error: Here is an error :->
點此 看 demo。
總整理
function*
是建立產生器的語法,稱為 generator function。- 產生器是可迭代的,因此可以用產生器改寫迭代器。
yield*
委派(delegate)將迭代的執行交給接在後面的產生器或可迭代的物件。- 利用產生器委派迭代任務給另一個產生器,這麼做的好處是不需要額外的變數(也就是記憶體)來暫存中間產物。
- 可使用
next/yield
來改變產生器的結果。 - 在實際生活上,非同步的產生器是比較常用的,因為可用於在
await ... of
迴圈處理資料流,像是從伺服器取得分頁資料的狀況。