產生器函式
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 特性會是 true,value 特性會是 undefined,如果 return 方法有指定引數,那麼傳回的物件 done 特性會是 true,value 特性會是指定的引數。
類似地,產生器有個 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)
>
在 range 的 prototype 上,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
>


