產生器函式

August 13, 2022

在〈for/of 與 Iterator〉看過一個 range 函式的實作,透過迭代器來產生一組值,看來有點複雜,實際上這個需求,可以透過產生器(Generator)函式來達成:

function* range(start, end) {
    for(let i = start; i < end; i++) {
        yield i;
    }
}

let r = range(3, 8);
for(let n of r) {
    console.log(n);
}

function 之後加了個 * 符號,這表示這會是個產生器函式,只有在產生器函式中,才可以使用 yield

產生器的流程

就流程來看, range 函式首次執行時,使用 yield 指定一個值,然後回到主流程使用 console.log 顯示該值,接著流程重回 range 函式 yield 之後繼續執行,迴圈中再度使用 yield 指定值,然後又回到主流程使用 console.log 顯示該值,這樣的反覆流程,會直到 range 中的 for 迴圈結束為止。

顯然地,這樣的流程有別於函式中使用了 return,函式就結束了的情況。實際上,產生器函式執行後,會傳回一個迭代器物件,該物件實作了迭代器的協定,此物件具有 next 方法:

> function* range(start, end) {
...     for(let i = start; i < end; i++) {
.....           yield i;
.....   }
... }
undefined
> let g = range(2, 5);
undefined
> g.next();
{ value: 2, done: false }
> g.next();
{ value: 3, done: false }
> g.next();
{ value: 4, done: false }
> g.next();
{ value: undefined, done: true }
>

由於產生器本身就是個迭代器,因此 for...of 實際上是對 range 傳回的迭代器進行迭代,它會呼叫 next 方法取得 yield 的指定值,直到下一個迭代出來的物件其 done 特性為 true 為止。因為每次呼叫迭代器的 next 時,迭代器才會運算並傳回下個產生值,因此就實現惰性求值效果而言,產生器函式的語法非常的方便。

除了以不帶引數的方式呼叫產生器的 next 方法之外,取得 yield 的右側指定值之外,還可以在呼叫 next 方法指定引數,令其成為 yield 的結果,也就是產生器可以給呼叫者值,呼叫者也可以指定值給產生器,這成了一種溝通機制。例如,設計一個簡單的生產者與消費者程式:

function* producer(n) {
    for(let data = 0; data < n; data++) {
        console.log('生產了:', data);
        yield data;
    }
}

function* consumer(n) {
    for(let i = 0; i < n; i++) {
        let data = yield;
        console.log('消費了:', data);
    }
}

function clerk(n, producer, consumer) {
    console.log(`執行 ${n} 次生產與消費`);
    let p = producer(n);
    let c = consumer(n);
    c.next();
    for(let data of p) {
        c.next(data);
    }
}

clerk(5, producer, consumer);

這個範例程式示範了如何應用產生器與 yield,以便在多個流程之間溝通合作。由於 next 方法若指定引數,會是 yield 的運算結果,因此 clerk 流程中必須先使用 c.next(),使得流程首次執行至 consumer 函式中 let data = yield 處先執行 yield,這會令流程回到 clerk 函式,之後 for...of 中會呼叫 p.next(),這時流程進行至 producer 函式的 yield data,在 clerk 取得 data 之後,接著執行 c.next(data) ,這時流程回到 consumer 之前 let data=yield 處,next 方法的指定值此時成為 yield 的結果。一個執行結果如下:

執行 5 次生產與消費
生產了: 0
消費了: 0
生產了: 1
消費了: 1
生產了: 2
消費了: 2
生產了: 3
消費了: 3
生產了: 4
消費了: 4

如果打算建立一個產生器函式,然而資料來源是直接從另一個產生器取得,那會怎麼樣呢?舉例來說,先前的range 函式就是傳回產生器,而你打算建立一個 np_range 函式,可以產生指定數字的正負範圍,但不包含 0:

function* range(start, end) {
    for(let i = start; i < end; i++) {
        yield i;
    }
}

function* np_range(n) {
    for(let i of range(0 - n, 0)) {
        yield i
    }

    for(let i of range(1, n + 1)) {
        yield i
    }
}

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

因為 np_range 必須得是個產生器,結果就是得逐一從來源產生器取得資料,再將之 yield,像是這邊重複使用了 for...of 來迭代並不方便,你可以直接使用 yield* 改寫如下:

function* range(start, end) {
    for(let i = start; i < end; i++) {
        yield i;
    }
}

function* np_range(n) {
    yield* range(0 - n, 0);
    yield* range(1, n + 1);
}

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

當需要直接從某個產生器取得資料,以便建立另一個產生器時,yield* 可以作為直接銜接的語法。

return/throw

如果定義產生器函式的時候使用了 return 會如何?

function* range(start, end) {
    for(let i = start; i < end; i++) {
        yield i;
    }
    return 'return';
}

for 迴圈之後,程式最後使用 return 試圖傳回 'return' 字串,來看看執行的結果:

> let g = range(1, 3);
undefined
> g.next();
{ value: 1, done: false }
> g.next();
{ value: 2, done: false }
> g.next();
{ value: 'return', done: true }
>

可以看到,產生器最後迭代出來的物件中,value 特性是 'return',而 done 特性被設為 true。回想一下,對於 for/of,在看到 done 特性後就停止,因此若是在 for...of 中,並不會迭代出 return 指定的值。

> let g2 = range(1, 3);
undefined
> for(let i of g2) {
...     console.log(i);
... }
1
2
undefined
>

你可以使用產生器的 return 方法,要求產生器直接 return,例如:

> function* range(start, end) {
...     for(let i = start; i < end; i++) {
.....         yield i;
.....     }
... }
undefined
> let g = range(1, 3);
undefined
> g.next();
{ value: 1, done: false }
> g.return();
{ value: undefined, done: true }
> let g2 = range(1, 3);
undefined
> g2.next();
{ value: 1, done: false }
> g2.return(10);
{ value: 10, done: true }
>

return 方法會在先前 yield 處進行 return,此時傳回的物件 done 特性會是 truevalue 特性會是 undefined,如果 return 方法有指定引數,那麼傳回的物件 done 特性會是 truevalue 特性會是指定的引數。

類似地,產生器有個 throw 方法,它會在先前 yield 處,將 throw 方法指定的值進行 throw,例如:

> let g = range(1, 3);
undefined
> g.next();
{ value: 1, done: false }
> g.throw('Orz');
Thrown: Orz
> let g2 = range(1, 3);
undefined
> g2.next();
{ value: 1, done: false }
> g2.throw(new Error('XD'));
Error: XD
    at repl:1:10
    at ContextifyScript.Script.runInThisContext (vm.js:50:33)
    at REPLServer.defaultEval (repl.js:240:29)
    at bound (domain.js:301:14)
    at REPLServer.runBound [as eval] (domain.js:314:12)
    at REPLServer.onLine (repl.js:441:10)
    at emitOne (events.js:121:20)
    at REPLServer.emit (events.js:211:7)
    at REPLServer.Interface._onLine (readline.js:282:10)
    at REPLServer.Interface._line (readline.js:631:8)
>

產生器的原型

產生器實作了迭代器的協定,如果你試著檢驗產生器,會發現它是產生器函式的實例,產生器的原型物件就是產生器函式的 prototype

> let g = range(1, 3);
undefined
> g instanceof range;
true
> g.__proto__ == range.prototype;
true
>

話雖如此,你不能對一個產生器函式使用 new

> let g2 = new range(1, 3);
TypeError: range is not a constructor
    at repl:1:10
    at ContextifyScript.Script.runInThisContext (vm.js:50:33)
    at REPLServer.defaultEval (repl.js:240:29)
    at bound (domain.js:301:14)
    at REPLServer.runBound [as eval] (domain.js:314:12)
    at REPLServer.onLine (repl.js:441:10)
    at emitOne (events.js:121:20)
    at REPLServer.emit (events.js:211:7)
    at REPLServer.Interface._onLine (readline.js:282:10)
    at REPLServer.Interface._line (readline.js:631:8)
>

rangeprototype 上,constructor 特性也不是參考 range

> range.prototype.constructor === range;
false
>

由於不能將產生器函式當成是建構式,因此在產生器函式中撰寫 this 的意義並不大,因為直接呼叫函式的話,this 會是 undefined

> function* range(start, end) {
...     for(this.i = start; this.i < end; this.i++) {
.....         yield this.i;
.....     }
... }
undefined
> let g = range(1, 3);
undefined
> g.next();
TypeError: Cannot set property 'i' of undefined
    at range (repl:2:16)
    at range.next (<anonymous>)
    at repl:1:3
    at ContextifyScript.Script.runInThisContext (vm.js:50:33)
    at REPLServer.defaultEval (repl.js:240:29)
    at bound (domain.js:301:14)
    at REPLServer.runBound [as eval] (domain.js:314:12)
    at REPLServer.onLine (repl.js:441:10)
    at emitOne (events.js:121:20)
    at REPLServer.emit (events.js:211:7)
>

雖然說,可以使用 call 方法來指定 this

> let o = {};
undefined
> let g = range.call(o, 1, 3);
undefined
> g.next();
{ value: 1, done: false }
> o.i;
1
> g.next();
{ value: 2, done: false }
> o.i;
2
> g.i;
undefined
>

然而,this 綁定的物件跟產生器沒有關係,若你真的有相似的需求,何不直接讓它明確一點呢?

> function* range(obj, start, end) {
...     for(obj.i = start; obj.i < end; obj.i++) {
.....         yield obj.i;
.....     }
... }
undefined
> let o = {};
undefined
> let g = range(o, 1, 3);
undefined
> g.next();
{ value: 1, done: false }
> o.i;
1
> g.next();
{ value: 2, done: false }
> o.i;
2
>

分享到 LinkedIn 分享到 Facebook 分享到 Twitter