[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 是一種特殊的函式,形式上和一般的函式一樣,但是有兩個特徵:
- function 關鍵字與函數名之間有一個星號(
*
); - 函數內部使用
yield
語句,定義不同的內部狀態(yield 在英語裡的意思就是「產出」)。
- function 關鍵字與函數名之間有一個星號(
- 執行 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),且此時done
為true
。 - 如果該函數沒有 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');