跳至主要内容

[JS] JavaScript Generator 的使用

/**
* Generator Function 可以透過 '*' 來宣告
* 執行 Generator 後會回傳 Iterator。
**/

function* g() {
// some code ...
let valueInNext = yield '<someValueInNext>';
console.log('valueInNext', valueInNext);
// some code ...
return 'ending'; // 會是最後一個 iterator 得到的 value
}

let iterator = g();

/**
* 當我們的 Iterator 第一次使用 next() 時,
* 函式會從頭執行到 function 中的第一個 yield。
**/

console.log(iterator.next()); // {value: '<someValueInNext>', done: false}

/**
* next('<value'>) 裡面代入參數時,會把回傳給 iterator 中的 yield
**/
console.log(iterator.next('valuePassToYield')); // { value: 'ending', done: true }

建立 Generator Function

  • Generator 是一種特殊的函式,形式上和一般的函式一樣,但是有兩個特徵:
    1. function 關鍵字與函數名之間有一個星號(*);
    2. 函數內部使用 yield 語句,定義不同的內部狀態(yield 在英語裡的意思就是「產出」)。
  • 執行 Generator 函式的方法和一般函式一樣,只需要在函式後面加上 () 括號。不同的是,呼叫 Generator 函數後,該函數並不執行,返回的也不是函數運行結果,而是一個指向內部狀態的指針物件,這個指針物件實際上是一個 [iterator](/Users/pjchen/Projects/Notes/source/_posts/JavaScript/[JS] JavaScript 疊代器(Iterator).md) 。
  • 下一步,必須呼叫疊代器物件的 next 方法,使得指針移向下一個狀態。也就是說,每次呼叫 next 方法,內部指針就從函數頭部或上一次停下來的地方開始執行,直到遇到下一個 yield 語句(或 return 語句)為止。換言之,Generator 函式是分段執行的,yield 語句是暫停執行的標記,而 next 方法可以恢復執行
function* helloWorldGenerator() {
// 第一次 next 開始
console.log('before yield hello');
let paramsInNextAfterHello = yield 'hello';
// 第一次 next 結束, yield 完後停在這裡

// 第二次 next 開始
console.log('paramsInNextAfterHello', paramsInNextAfterHello);
let paramsInNextAfterWorld = yield 'world';
// 第二次 next 結束, yield 完後停在這裡

// 第三次 next 開始
console.log('paramsInNextAfterWorld', paramsInNextAfterWorld);
return 'ending';
// 第三次 next 結束, yield 完後停在這裡

// 之後的 next 都會回傳 {done: true, value: undefined}
console.log('after yield ending'); // 如果上面用 return 則不會執行到這行
}

var hw = helloWorldGenerator();
hw.next(); // before yield hello, Object {value: "hello", done: false}
console.log('------');
hw.next('after yield hello'); // after yield hello, Object {value: "world", done: false}
console.log('------');
hw.next('after yield world'); // after yield world, Object {value: "ending", done: true}
console.log('------');
hw.next(); // Object {value: undefined, done: true}
console.log('------');
hw.next(); // Object {value: undefined, done: true}

Sample Code

Create a Generator Function @ JSFiddle

yield 的使用

  • 疊代器物件的 next 方法的運行邏輯如下

    • 遇到 yield 語句,就暫停執行後面的操作,並將緊跟在 yield 後面的表達式的值,作為返回物件的屬性值(value
    • 下一次呼叫 next 方法時,再繼續往下執行,直到遇到下一個 yield 語句。
    • 如果沒有再遇到新的 yield 語句,就一直運行到函數結束,直到 return 語句為止,並將 return 語句後面表達式的值,作為返回物件的屬性值(value),且此時 donetrue
    • 如果該函數沒有 return 語句,value 為最後一個 yield 後表達式的值,但此時 done 仍為 false,要到下一個 next 後 done 才會是 true。
  • yield 語句用作函數參數或放在賦值表達式的右邊,可以不加括號

function* demo() {
let input = yield; // OK
}
  • yield 句本身沒有返回值,或者說總是返回 undefined
let a = yield 'Hello'      // a === undefined
  • next 方法可以帶一個參數,該參數就會被當作上一個在 generator 中 yield 語句的返回值。
g.next(3); // a === 3

注意,由於 next 方法的參數表示上一個 yield 語句的返回值,所以第一次使用 next 方法時,不能帶有參數

  • V8 引擎直接忽略第一次使用 next 方法時的參數,只有從第二次使用 next 方法開始,參數才是有效的。從語義上講,第一個 next 方法用來啟動疊代器物件,所以不用帶有參數。
/**
* yield 本身沒有返回值,或者說總是返回 undefined
* 但我們可以透過代入 next('<value'>) 來給 yield 值
**/

function* f() {
for (var i = 0; true; i++) {
console.log('i-before reset', i);
let reset = yield i;
console.log('i-after reset', i);
console.log('reset ' + reset + ', i ' + i);
if (reset) {
i = -1;
}
}
}

var g = f();

g.next(); // "i-before reset 0", Object {value: 0, done: false}
g.next(); // "i-after reset 0", "reset undefined", "i-before reset 1", i 0, Object {value: 1, done: false}
g.next(); // "i-after reset 1", "reset undefined, i 1", "i-before reset 2", Object {value: 2, done: false}
g.next(true); // "i-after reset 2", "reset true, i 2", "i-before reset 0", Object {value: 0, done: false}
g.next(); // "i-after reset 0", "reset undefined, i 0", "i-before reset 1" Object {value: 1, done: false}

在看下面的例子

/**
* V8 引擎直接忽略第一次使用 next 方法時的參數,
* 只有從第二次使用 next 方法開始,參數才是有效的。
* 從語義上講,第一個 next 方法用來啟動疊代器物件,
* 所以不用帶有參數。
**/

function* f2() {
for (var i = 0; true; i++) {
let reset = yield i;
console.log('afterReset ' + reset + ', i ' + i);
if (reset) {
i = -1;
}
}
}

var g2 = f2();

g2.next('a'); // Object {value: 0, done: false}
g2.next('b'); // "afterReset b, i 0", Object {value: 0, done: false}
g2.next('c'); // "afterReset c, i 0", Object {value: 0, done: false}
g2.next(true); // "afterReset true, i 0", Object {value: 0, done: false}
g2.next('e'); // "afterReset e, i 0", Object {value: 0, done: false}

返回結果

[object Object] {
done: false,
value: 0
}
"-----------"
"reset b, i 0"
[object Object] {
done: false,
value: 0
}
"-----------"
"reset c, i 0"
[object Object] {
done: false,
value: 0
}
"-----------"
"reset true, i 0"
[object Object] {
done: false,
value: 0
}
"-----------"
"reset e, i 0"
[object Object] {
done: false,
value: 0
}
  • 另外,yield 語句如果用在一個表達式之中,必須放在圓括號裡面(yield 123)。
// yield 語句如果用在一個表達式之中,必須放在圓括號裡面。
function* demo() {
console.log('Hello' + yield); // SyntaxError
console.log('Hello' + yield 123); // SyntaxError

console.log('Hello' + (yield)); // OK
console.log('Hello' + (yield 123)); // OK
}

How to use yield in Generator Function @ PJCHENder JSFiddle

實際應用

搭配 while 使用,讓該 iterator 不會進入 done

在 generator function 中使用 while (true) { ... } 這種寫法時,可以製造出一個不會終止的 iterator,也就是 done 一直都會是 false,因此可以不斷的透過 next('<value>') 把資料帶入 generator 中:

function* infiniteGenerator() {
while (true) {
const result = yield;
console.log('get result: ', result);
}
}

// Initialize the generator
const fetchResult = infiniteGenerator();
fetchResult.next();

// Feed result as parameter of next
fetchResult.next('foo');
fetchResult.next('bar');

Sample Code

搭配 for 迴圈,在 iterator 執行一定次數後才做下一件事

function* generatorWithForLoop() {
for (let i = 1; i <= 3; i++) {
const result = yield i;
console.log('result', result);
}

console.log('Start do something here...');
}

在 Generator function 中使用 for 迴圈,可以確保該 iterator 執行特定次數後,才會開始 Start do something

搭配 for...of,使用 Generator Function 建立 Iterator

  • for...of 可以自動疊代 Generator 函數時生成的 Iterator 物件,且此時不再需要呼叫 next 方法
  • 下面代碼使用 for...of 迴圈,依次顯示 5 個 yield 語句的值。這裡需要注意,一旦 next 方法的返回物件的 done 屬性為 true 時,for...of 就會中止,且不包含該返回物件,所以上面代碼的 return 語句返回的 6,不包括在 for...of 之中
/**
* 一旦 next 方法的返回物件的 done 屬性為 true時,
* for...of 疊代就會中止,且不包含該返回物件
**/
function* foo() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
return 6;
}

for (let v of foo()) {
console.log(v); // 1 2 3 4 5
}

Sample Code

利用 Generator Function 建立客制化的 Iterator:

let obj = {
[Symbol.iterator]: g,
firstName: 'Aaron',
lastName: 'Chen',
hobbies: ['programming', 'biking', 'sports'],
};

function* g() {
let hobbies = this.hobbies;
for (let i = 0; i < hobbies.length; i++) {
yield hobbies[i];
}
}

for (let item of obj) {
console.log(item); // programming, biking, sports
}

Sample Code

透過 Generator Function 仿造 Object.Entries() 方法

function* objectEntries(obj) {
let propKeys = Reflect.ownKeys(obj);

for (let propKey of propKeys) {
yield [propKey, obj[propKey]];
}
}

let jane = { first: 'Jane', last: 'Doe' };

for (let [key, value] of objectEntries(jane)) {
console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe

加上疊代器接口的另一種寫法是,將 Generator 函數加到物件的 Symbol.iterator 屬性上面

function* objectEntries() {
let propKeys = Object.keys(this);

for (let propKey of propKeys) {
yield [propKey, this[propKey]];
}
}

let jane = { first: 'Jane', last: 'Doe' };

jane[Symbol.iterator] = objectEntries;

for (let [key, value] of jane) {
console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe

Sample Code

Error Handling in Generator

/**
* Error Handling in Generator
* 我們可以用 iterator.throw() 搭配 try...catch。
* 另外,也可以用 iterator.return() 來改變 value 的值
**/

let hobbies = ['programming', 'biking', 'sports', 'computer', 'royal host'];

function* g(hobbies) {
for (let i = 0; i < hobbies.length; i++) {
try {
let valueInNext = yield hobbies[i];
console.log('valueInNext', valueInNext);
} catch (e) {
console.log('error:', e);
}
}
}

let iterator = g(hobbies);
iterator.next(); // Object {value: "programming", done: false}
iterator.throw('Some thing Wrong'); // "error: Some thing Wrong", Object {value: "biking", done: false}
iterator.next('favorite'); // "valueInNext favorite", Object {value: "sports", done: false}
iterator.next(); // "valueInNext undefined", Object {value: "computer", done: false}
iterator.return('Give Value Inside'); // Object {value: "Give Value Inside", done: true}
iterator.next(); // Object {value: undefined, done: true}

Error Handling with Try Catch in Generator @ PJCHENder JSFiddle

Demo Code

Create a Generator Function @ JSFiddle How to use yield in Generator Function @ PJCHENder JSFiddle Use Generator with for...of @ PJCHENder JSFiddle Error Handling with Try Catch in Generator @ PJCHENder JSFiddle

資料來源

Generator 函數的語法 @ ECMA Script 入門 by 阮一峰