在〈for…of 與迭代器〉中曾經看過一個 range
函式的實作,透過迭代器來產生一組值,看來有點複雜,實際上這個需求,在 ES6 中可以透過產生器(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*
可以作為直接銜接的語法。