產生器函式
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
>