寫在前面
其實之前在 [黯羽輕揚:JavaScript 生成器](/articles/javascript 生成器/) 中已經總結過了,這裡並非故意重複。之前是參考 MDN 文檔做出的總結,偏重語法規則,本文根據 FireFox 中該特性的實現者的親述,補充一些細節和應用場景
為了避免重複,本文不再解釋語法規則(function* + yield),語法細節請查看之前的文章
一。作用及內部原理
generator(生成器)用來創建迭代器,語法非常簡潔(function* + yield)
生成器執行 yield 語句時,生成器的堆棧結構(本地變量、參數、臨時值、生成器內部當前的執行位置)被移出堆棧。但生成器對象保留了對這個堆棧結構的引用(備份),所以稍後調用 .next() 可以重新激活堆棧結構並且繼續執行
例如:
// 定義生成器
var gen = function*() {
console.log('before yield 1');
yield 1;
console.log('before yield 2');
yield 2;
}
// 調用生成器返回迭代器
var iter = gen();
iter.next(); // before yield 1
// Object {value: 1, done: false}
iter.next(); // before yield 2
// Object {value: 2, done: false}
iter.next(); // Object {value: undefined, done: true}
iter.next(); // Object {value: undefined, done: true}
yield 語句把函數體分割成了幾段,.next() 一次執行一段
二。迭代器與生成器
function* 定義的東西叫迭代器的生成器(簡稱生成器),因為調用它返回一個迭代器
所有的生成器都有內建 .next() 和 [Symbol.iterator]() 方法的實現,我們只需要編寫循環部分的行為。function* 後面的函數體就像循環結構的循環體,例如:
function* gen(arr) {
for (var i = 0; i < arr.length; i++) {
yield arr[i];
}
}
var iter = gen([1, 2, 4]);
console.log(iter.next()); // Object {value: 1, done: false}
console.log(iter.next()); // Object {value: 2, done: false}
其中 gen 的作用就是把連續的數組變成「會喘氣的」數組,for 循環本來是停不下來的,但 yield 確實讓它停下來了,這也是生成器的一大特色。利用這個特點可以實現很多有趣的東西,比如用動畫展示快速排序的過程,偽代碼如下:
function quicksort(arr) {
// sort
forloop {
updateSortedArr(); // 完成一趟排序
displaySortedArr(); // 展示本趟排序結果
}
return sortedArr;
}
當然,這樣無法看到動畫。因為 n 趟排序瞬間就完成了(太快了,啥都看不清啊哪有動畫啊,根本沒變好嗎)。很容易想到變通的方法:先把每一趟的結果存起來,最後再展示:
function quicksort(arr) {
var tmpArr = [];
forloop {
updateSortedArr(); // 完成一趟排序
tmpArr.push(sortedArr); // 存起來
}
// 動畫展示排序過程
anim(tmpArr);
}
拿到每一趟的結果後,想怎麼展示就怎麼展示。感覺也不很費勁,那好,如果要動畫展示快速排序每一趟 2 個指針的移動呢?我們好像又需要記錄指針移動的 tmpArr 了,如果這個數組很大,如果。。。這樣勢必需要更多的內存空間來記錄已經發生過的事情
仔細想想,排序過程中,動畫展示需要的數據都已經有了,那能不能邊排序邊動畫展示呢?當然可以:
function* quicksort(arr) {
forloop {
yield sortedArr;
// 或者
// yield step;
}
}
var iter = quicksort(arr);
// 動畫展示排序過程
function anim() {
display(iter.next());
setTimeout(anim, 300);
}
anim();
沒錯,生成器讓循環能「喘氣」了,在喘氣過程中製造動畫
P.S. 其實就算沒有生成器,我們也能讓循環「停」下來,請查看 [黯羽輕揚:JavaScript 實現 yield](/articles/javascript 實現 yield/)
P.S. 關於快速排序的具體細節,請查看 黯羽輕揚:排序算法之快速排序(Quicksort)解析
三。特點
生成器的特點如下:
-
普通函數不能自暫停,生成器函數可以
-
yield只在function*的直接作用域中有效,function*中匿名函數中的yield是非法的 -
可以處理無限序列。無法構造無限大的數組,但可以用生成器實現無限序列的構造規則,以處理無限序列
-
提供了返回數組的另一種思路,返回生成器而不是數組,用時間換空間
-
可以重構複雜循環,把它拆分成 2 個部分,把數據生成部分轉換為生成器,然後 for...of 遍歷這些數據
-
可以快速製造迭代器,讓任意對象可迭代,具體請查看 [黯羽輕揚:for…of 循環_ES6 筆記 1](/articles/for-of 循環-es6 筆記 1/)
-
便於擴展迭代器,很容易實現過濾等操作
第 2 點需要注意,因為大多數介紹生成器的資料都不會提及這一點,但我們確實無法在生成器中 setTimeout 延遲 yield。用生成器擴展迭代器是個不錯的選擇,代碼很自然,示例如下:
// 擴展迭代器
function* filter(isValid, iterable) {
for (var val of iterable) {
if (isValid(val)) {
yield val;
}
}
}
// test
function isValid(val) {
return val > 1;
}
for (var val of filter(isValid, [0, 1, 2, 4])) {
console.log(val);
}
用生成器包裝迭代器,有種無縫銜接的美感,不是嗎?
四。高級技巧
1. 從外部影響生成器的邏輯流
迭代器的 next(returnVal) 方法接受可選參數,參數會作為生成器中上一條 yield 語句的返回值,這樣調用者就可以從外部影響生成器的邏輯流,例如:
function* gen() {
var water = yield 'give me a cup of pure water';
yield water.drink();
}
// test
var iter = gen();
console.log(iter.next());
console.log(iter.next({
name: 'pure water',
drink: function() {
return 'hmm, well';
}
}));
第一個 yield 向調用者要一杯水,第二個 .next() 把水遞給生成器了,然後第二個 yield 把水喝掉了
這是一個雙向的交互過程,實際應用中,根據 next() 的返回值判斷生成器需要什麼,再通過下一個 .next() 傳遞給它,把複雜邏輯隔離在生成器中,調用起來就像對話一樣輕鬆
2. 終止迭代器
注意,是迭代器,有兩種方法終止一個迭代器:
-
迭代器的
throw(err)方法,效果像是生成器中 yield 表達式調用一個函數並拋出錯誤 -
迭代器的
return(returnVal)方法,接受可選參數,參數會作為 value 返回(done為true),生成器只執行finally代碼塊並不再恢復執行
throw 示例如下:
// throw
function* gen() {
try {
yield 1;
yield 2;
} catch (err) {
console.log('error occurs: ' + err);
} finally {
console.log('clean up');
}
}
var iter = gen();
console.log(iter.next()); // Object {value: 1, done: false}
console.log(iter.throw(new Error('err'))); // error occurs: Error: err
// clean up
// Object { value: undefined, done: true }
console.log(iter.next()); // Object {value: undefined, done: true}
throw(err) 表示迭代器異常關閉,用於通知生成器內部執行 finally 中的清理工作
而 return() 則表示迭代器正常關閉,示例如下:
console.log(iter.next()); // Object {value: 1, done: false}
console.log(iter.return('ok')); // clean up
// Object { value: "ok", done: true }
console.log(iter.next()); // Object {value: undefined, done: true}\n
注意:'ok' 作為 value 立即返回,而不是下一個 .next() 時返回
P.S. Chrome49 還是不支持 return(),FF 下 throw() 後仍然可以 return(),而如果先 return() 再 throw() 會報錯
3. 拼接迭代器
yield* iter 可以拼接迭代器,支持在一個生成器中調用另一個生成器,例如:
var gen1 = function* (){
yield 1;
yield 2;
}
var gen2 = function* (){
yield* gen1();
yield 3;
yield 4;
}
for (var val of gen2()) {
console.log(val); // 1 2 3 4
}
五。總結
生成器能讓執行流「喘口氣」,能讓停不下來的東西暫停,能用來重構循環,能駕馭無限序列,能包裝迭代器。。。好處多多
參考資料
- 《ES6 in Depth》:InfoQ 中文站提供的免費電子書
暫無評論,快來發表你的看法吧