跳到主要內容
黯羽輕揚每天積累一點點

generator(生成器)_ES6 筆記 2

免費2016-04-03#JS#js生成器#生成器用法#JavaScript生成器

根據 FireFox 中該特性的實現者的親述,補充一些細節和應用場景

寫在前面

其實之前在 [黯羽輕揚: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 返回(donetrue),生成器只執行 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 中文站提供的免費電子書

評論

暫無評論,快來發表你的看法吧

提交評論