簡介產生器函式


在〈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* 可以作為直接銜接的語法。