JavaScript ES6 Generators 生成器

雖然 Iterators (迭代器) 是 ES6 很強大的新功能,但寫起來會有點小麻煩,因為你需要自己維護一個內部的指針狀態 (internal state)。

Generators (生成器) 是 ES6 引入的另一個新功能,Generators 像是一個可以隨時暫停 (pause) 和繼續 (resume) 執行的特殊函數,它會自動幫你維護一個內部狀態。

先來看看 Generator 的語法:

function* gen() { 
    yield 1;
    yield 2;
    yield 3;
}

var g = gen();

上面是一個 Generator 的宣告,看起來就像是一般的函數,但有幾點不同:

  • 宣告一個 Generator 的關鍵字是 function*,function 關鍵字後面接著一個星號 *
  • Generator 函數裡面使用 yield 關鍵字,來定義和暫停不同的內部執行狀態

var g = gen(); 執行 Generator 函數會返回一個 Generator Object,其本質上是一個 Iterator

一個 Generator 的運作有點像是一個狀態機 (state machine),會一直改變內部的不同狀態:起始狀態 -> 繼續 -> 暫停 (狀態 1) -> 繼續 -> 暫停 (狀態 2) -> 繼續 -> .... -> 結束 (狀態 N)

一個 Generator Object 的起始狀態會先暫停什麼都不做,而每次執行 next() 方法,就會繼續執行函數,直到遇到下一個 yield 關鍵字,又會暫停函數的執行,而每一次暫停時會 yield (產出) 一個當前狀態值。

和一般 Iterator 一樣,yield 會返回一個 {value: anyType, done: boolean} 結構的物件。

// 第一次執行 next() - 停在 yield 1
//
// 返回 Object {value: 1, done: false}
g.next();

再繼續執行:

// 停在 yield 2
//
// 返回 Object {value: 2, done: false}
g.next();

再繼續執行:

// 停在 yield 3
//
// 返回 Object {value: 3, done: false}
g.next();

再繼續執行:

// 返回 Object {value: undefined, done: true}
g.next();

跟一般函數一樣,Generator 函數執行到結尾或遇到 return 就會結束 (狀態 done=true)。

再確認一下,是不是真的結束了:

// iterator 結束了,再執行 next() 狀態也不會再改變
//
// 還是返回 Object {value: undefined, done: true}
g.next();

Infinite Iterator / Yielding Indefinitely

Generator 也可以無限永遠的執行下去:

function* idMaker() {
    var index = 0;

    while(true) {
        yield index++;
    }
}

var gen = idMaker();

console.log( gen.next().value ); // 0
console.log( gen.next().value ); // 1
console.log( gen.next().value ); // 2
// ...

上面的 Generator 永遠不會結束,會一直 yield 新值 (value)。

for...of

Generator object 實作了 Iterables/Iterators 接口,所以可以被 for...of

function* idMaker() {
    var index = 0;

    while(true) {
        yield index++;
        
        if (index > 3) {
            break;
        }
    }
}

for (let i of idMaker()) {
    console.log(i);
}

// 依序輸出 0 1 2 3

yield

我們不僅可以從 generator 裡面 yield 值出去外面,也可以從外面傳值進去 generator。

next(value) 方法可以接受一個參數,當作上一次 yield 語句的返回值:

例子:

function* gen() {
    var index = 0;
    
    while(true) {
        var value = yield index++;
        
        console.log('Value from outsise: ' + value);
    }
}

var g = gen();

console.log('Value from insise: ' + g.next(10).value);
// 將 10 傳進 generator

console.log('Value from insise: ' + g.next(20).value);
// 將 20 傳進 generator

console.log('Value from insise: ' + g.next(30).value);
// 將 30 傳進 generator

上面的例子依序會輸出:

Value from insise: 0
Value from outsise: 20
Value from insise: 1
Value from outsise: 30
Value from insise: 2

注意不會輸出 "Value from outsise: 10",因為 next 傳入的值是當作 "上一次" yield 的返回值。

yield 關鍵字也可以放在一般的表達式 (expression) 中:

function* gen() {
    let arrayOfYields = [yield, yield, yield];
    
    console.log(arrayOfYields);
}
 
let g = gen();

g.next();
g.next('JavaScript');
g.next('ES6');
g.next('Awesome');
// 輸出 ["JavaScript", "ES6", "Awesome"]

yield 的優先權 (precedence) 很低,除了像上面的例子放在陣列中或像是當作函數參數,大多數的情況下你需要加個小括號避免意外錯誤。

yield a + b + c;
// 會被看作是
yield (a + b + c);
// 而不是
(yield a) + b + c;

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

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

yield*

Generators 就像是一般的函數,可以用 return 來強制返回並中斷執行。

function* gen() {
    yield 1;
    yield 2;
    return 3;
    yield 4;
}

let g = gen();

// Object {value: 1, done: false}
g.next();

// Object {value: 2, done: false}
g.next();

// Object {value: 3, done: true}
g.next();

// Object {value: undefined, done: true}
g.next();

從上面可以看到 return 會返回值同時將 iterator 狀態設為結束 (done: true)。

但用 return 你可能會遇到一個問題就是,當你遍歷這個 generator object 時,不會得到 return 的值,因為一般遍歷相關的方法 (像是 for...of),只要遇到 done=true 就會立刻結束遍歷。

for (let v of gen()) {
    console.log(v);
}

// 你只會看到輸出
// 1
// 2

那你可能會問那 return 有啥用途?return 可以搭配 yield* 來使用。

yield* 這關鍵字可以用來在一個 generator 裡面執行另一個的 generator。

yield* 範例:

function* g1() {
    yield 2;
    yield 3;
    yield 4;
}

function* g2() {
    yield 1;
    yield* g1();
    yield 5;
}

var iterator = g2();

for (let v of g2()) {
    console.log(v);
}

// 依序會輸出 1 2 3 4 5

而 yield* expression 的返回值,就是執行的 generator object 的 return value:

function* g4() {
    yield* [1, 2, 3];

    // 返回 foo 給外面的 yield*
    return 'foo';
}

var result;

function* g5() {
    // result 的值是 g4() 的 return value
    result = yield* g4();
}

var iterator = g5();

// {value: 1, done: false}
console.log(iterator.next());

// {value: 2, done: false}
console.log(iterator.next());

// {value: 3, done: false}
console.log(iterator.next());

// {value: undefined, done: true}, 
console.log(iterator.next());

// result 的值是 "foo"
console.log(result);

Generator.prototype.throw() / Generator.prototype.return()

Generator.prototype.throw() 和 Generator.prototype.return() 是 generator object 上的方法,用來改變 generator 內部的程式流程。

Generator.prototype.throw()

throw() 像是 next() 可以用來繼續執行 generator,但 throw() 執行的方式是從外部拋出錯誤給 generator 內部來捕捉。throw() 還可以傳入一個參數給 generator 內部的 catch 來接收。

throw() 和 next() 一樣是繼續執行 generator,直到遇到下一個 yield 或 return,然後返回 {value: anyType, done: boolean}。

function* gen() {
    while(true) {
        try {
            yield 42;

        } catch(e) {
            console.log('Error caught!');
        }
    }
}

var g = gen();

// { value: 42, done: false }
g.next();

// 接著用 throw() 拋出一個錯誤進 generator
// generator 內部會切換到 catch 區塊
// 然後顯示 "Error caught!"
// 接著繼續執行程式,到下一個 yield 處

// { value: 42, done: false }
g.throw(new Error('Something went wrong'));

Generator.prototype.return()

return() 用來直接返回指定的值,並終止 generator 的執行狀態 (done=true)。

function* gen() { 
    yield 1;
    yield 2;
    yield 3;
}

var g = gen();

// { value: 1, done: false }
g.next();

// { value: "foo", done: true }
g.return('foo');

// { value: undefined, done: true }
g.next();