暫停一下再出發!全面解析 JavaScript Generator 實用技巧
什麼是 Generator?
Generator 的基本概念
在現實生活中,很多情境需要我們「做一點事、暫停一下、再繼續做下一點事」,例如:
- 填問卷:先回答前幾題,暫時休息,等想到答案後再繼續填。
- 做菜:準備食材、放進鍋裡煮,等熟了之後再進行下一個步驟。
在程式世界裡,如果我們希望「做一點事、暫停一下、再繼續做下一點事」就需要依賴 Generator。Generator 就像幫我們建立了一個「可隨時暫停和繼續」的函式。透過在函式宣告前面加上 *(例如 function* myGenerator())並在需要暫停的地方使用 yield 關鍵字,我們就可以一步步地控制這個函式的執行。
function* 與 yieldfunction*:這個宣告方式代表「這是一個生成器函式」,與普通的function最大差別在於,它能透過yield進行暫停與繼續。yield:可以想像成「把工作階段暫停,先把目前結果交出去,等之後再被叫起來繼續做」。程式每次跑到yield就會停住,直到有人呼叫next()才再往下繼續。
以下我們先舉一個簡單的「計數 器生成器」範例,帶大家感受一下 Generator 的神奇之處:
function* counter() {
let count = 0;
while (true) {
yield count; // 暫停並返回當前 count
count++;
}
}
const c = counter();
console.log(c.next()); // { value: 0, done: false }
console.log(c.next()); // { value: 1, done: false }
console.log(c.next()); // { value: 2, done: false }
以上的程式中,每次呼叫 c.next(),就像對計數器說:「往下跑一步,並告訴我現在的數字」。
為什麼需要 Generator?
我們已經知道 Generator 可以在函式執行過程中隨時暫停、繼續,那這對我們到底有什麼幫助呢?以下舉幾個常見的使用場景:
-
循序執行(順序產生資料)
假設你在寫一支程式,需要依序產生一大串數字或資料,就像流水線一樣,每做一個就回傳一次給外面使用者。這時候,Generator 可以很直覺地實現「一個一個丟出來」的功能,而不需要一次把所有資料都生成完才回傳。 -
延遲計算(按需計算資料)
在面 對龐大的資料或複雜的運算時,有時候我們不想一次算到完,因為那樣可能會非常耗時或耗記憶體。透過 Generator,我們可以在需要用到結果的時候,才去計算下一步。以餐廳廚房出餐作為比喻:通常一般的餐廳不會一次煮好一桌菜才出餐,而是先做好前菜就立即出餐,等待客人吃完再出下一道菜,減少廚房忙亂,也可節省資源。
-
提高程式碼可讀性
在某些需要階段性處理的流程中,如果不用 Generator,可能會寫得一團亂,或需要很多旗標變數來控制執行時機。Generator 讓我們可以在邏輯上更容易地一段一段看,每次yield都代表一個「休息切點」,這樣程式結構會變得較清晰。
Generator 的運作原理
迭代器與生成器的關係
在探討 Generator 的運作前,我們先簡單認識一下「迭代器(Iterator)」的概念。
- 迭代器:一個符合 Iteration protocols 的物件,能夠逐一取出內部的資料,每次往前「走一步」,給你下一個資料,直到資料走完為止。
- 在 JavaScript 裡,迭代器會實作一個
next()方法,每次呼叫都回傳一個{ value, done }物件。value表示當前的資料,done表示是否已到末端。
以下我們來用個簡單的範例示範如何「手動」實作一個簡單的迭代器,用來取得陣列中每個元素:
function createIterator(array) {
let index = 0;
return {
[Symbol.iterator]() {
return this;
},
next() {
if (index < array.length) {
return { value: array[index++], done: false };
} else {
return { value: undefined, done: true };
}
}
};
}
const myIterator = createIterator([1, 2, 3]);
console.log(myIterator.next()); // { value: 1, done: false }
console.log(myIterator.next()); // { value: 2, done: false }
console.log(myIterator.next()); // { value: 3, done: false }
console.log(myIterator.next()); // { value: undefined, done: true }
看上去跟前一章節 Generator 範例中的使用方法完全一樣對吧? 這是因為 Generator 函數返回的Generator Object ,其本質上就是一個迭代器物件。
Generator 幫我們自動實作了上述「迭代器介面」的繁瑣部分。只要我們用 function* 宣告函式,裡面寫上 yield,就能生成一個物件,內建了 next() 方法,無須手動管理索引或結束時機。每次呼叫 Generator 物件的 next(),回傳的結構也與一般迭代器相同: { value, done }。
function* myGenerator() {
yield 'Hello';
yield 'World';
}
const genObj = myGenerator();
console.log(genObj.next()); // { value: 'Hello', done: false }
console.log(genObj.next()); // { value: 'World', done: false }
console.log(genObj.next()); // { value: undefined, done: true }
可以看到,genObj 自帶 next() 方法,每次執行都回傳 { value, done },說明這個物件同時是「可迭代」的。
如果想更深入了解迭代器(Iterator)的規範與用法,建議可參考 MDN: Iterator 進行進一步閱讀。
yield 與 return
在 Generator 裏面,最常見也最重要的兩個關鍵字莫過於 yield 與 return。它們雖然都能「輸出」值,但實際上有著不同的意義與行為。
yield 的功能與特性:單向與雙向通信
-
單向:你可以在
yield後面寫一個值,例如yield "Hello",呼叫next()時,這個值會被「拋出」給外部。 -
雙向:如果外部在
next()裏面傳入參數,例如genObj.next("some data"),那麼該次執行的yield也能「接收」到這個參數。 -
範例程式(展示雙向通信):
function* twoWayGenerator() {
const firstData = yield 'Send me something';
console.log('I received:', firstData);
const secondData = yield 'One more time?';
console.log('I received:', secondData);
}
const gen = twoWayGenerator();
console.log(gen.next()); // { value: 'Send me something', done: false }
console.log(gen.next('Hi there')); // 在這裡傳入參數
// 主控台輸出: I received: Hi there
// => { value: 'One more time?', done: false }
console.log(gen.next('Yes!'));
// 主控台輸出: I received: Yes!
// => { value: undefined, done: true }
return 在 Generator 中的作用
-
生成器結束時的返回值
- 一旦
return someValue出現,整個 Generator 就直接結束,並透過{ value: someValue, done: true }回傳。 - 此後再呼叫
next(),done都會維持true,不會再有新的value。
- 一旦
-
return與yield的差異yield:暫停函式執行,但有機會再被「叫醒」繼續跑。return:一次性結束函式,相當於「收工」。- 結束時也會回傳一個
{ value, done: true }物件,但從此之後就不再有任何新輸出了。
-
範例:比較
yield與returnfunction* checkYieldAndReturn() {
yield 'First yield value'; // 先暫停一次
yield 'Second yield value'; // 再暫停一次
return 'Final return value'; // 結束並回傳
// 下面任何程式碼都不會再被執行到
yield 'This will never show';
}
const gen = checkYieldAndReturn();
console.log(gen.next()); // { value: 'First yield value', done: false }
console.log(gen.next()); // { value: 'Second yield value', done: false }
console.log(gen.next()); // { value: 'Final return value', done: true }
// 之後再怎麼呼叫,都不會再有新的結果
console.log(gen.next()); // { value: undefined, done: true }可以看到,當遇到
return 'Final return value'時,就一次性結束了生成器。
- 如果想繼續吐出更多值,就不要使用
return; - 如果只想在某個狀況下結束整個生成器,就可使用
return來做「中斷且回傳」的動作。