JavaScript 函式(Functions)─ 生成器(Generator)
簡介
在 JavaScript 的函式世界裡,**生成器(Generator)**是一種特殊的函式,讓我們可以「一次產出」多個值,而不必一次執行完所有程式碼。它結合了 迭代器(iterator) 的概念與 惰性求值(lazy evaluation) 的特性,使得資料流的控制變得更彈性,也能寫出更易讀、易維護的非同步程式碼。
對於需要處理大量資料、串流資料或是 非同步流程(例如 async/await 前的協程寫法),生成器提供了 可暫停、可恢復 的執行模型。掌握這項技術,能讓你在前端或 Node.js 專案中,寫出更具效能與可讀性的程式。
核心概念
1. 生成器函式的語法
生成器是一種以 function*(星號)宣告的函式。呼叫它不會直接執行函式本體,而是回傳一個 迭代器物件(iterator),此物件擁有 next()、return()、throw() 三個方法。
function* countUpTo(max) {
let i = 0;
while (i < max) {
// 每次呼叫 next() 時,執行到此處暫停,回傳 i
yield i;
i++;
}
}
yield:將當前值「產出」給呼叫端,同時暫停函式執行。- 呼叫
next()後,函式會從暫停的地方繼續往下執行,直到下一個yield或結束。
2. 迭代器的使用方式
const iterator = countUpTo(3); // 產生 iterator,尚未執行
console.log(iterator.next()); // { value: 0, done: false }
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: undefined, done: true }
value為yield的結果。done為布林值,表示是否已遍歷完全部yield。
3. for...of 直接遍歷生成器
生成器本身就是可迭代的(iterable),可以直接使用 for...of:
for (const n of countUpTo(5)) {
console.log(n); // 0 1 2 3 4
}
4. 生成器與非同步(Async Generator)
ES2018 引入 async function*,讓 yield 可以搭配 await,處理 非同步資料流(如讀檔、網路串流):
async function* fetchLines(url) {
const response = await fetch(url);
const reader = response.body.getReader();
const decoder = new TextDecoder();
let { value: chunk, done } = await reader.read();
let buffer = '';
while (!done) {
buffer += decoder.decode(chunk, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop(); // 最後一行可能不完整,保留在 buffer
for (const line of lines) {
yield line; // 每行文字一次產出
}
({ value: chunk, done } = await reader.read());
}
if (buffer) yield buffer; // 最後剩餘的資料
}
使用方式:
(async () => {
for await (const line of fetchLines('https://example.com/large.txt')) {
console.log('收到一行:', line);
}
})();
5. 生成器的 return() 與 throw()
iterator.return(value):強制結束生成器,返回{ value, done: true },同時執行finally區塊(如果有的話)。iterator.throw(error):在生成器內部拋出錯誤,可在try...catch中捕捉。
function* demo() {
try {
yield 1;
yield 2;
} finally {
console.log('清理資源');
}
}
const it = demo();
console.log(it.next()); // { value: 1, done: false }
console.log(it.return(99)); // 清理資源
// { value: 99, done: true }
程式碼範例
範例 1:簡易的斐波那契序列(無限生成)
function* fibonacci() {
let a = 0, b = 1;
while (true) {
yield a; // 產出當前值
[a, b] = [b, a + b]; // 更新 a、b
}
}
// 取前 10 個斐波那契數字
const fib = fibonacci();
for (let i = 0; i < 10; i++) {
console.log(fib.next().value);
}
重點:
while (true)讓生成器無限產出,外部自行決定何時停止。
範例 2:使用 yield* 代理子生成器
function* letters() {
yield 'a';
yield 'b';
yield 'c';
}
function* alphabet() {
yield* letters(); // 直接將 letters 的產出傳遞出去
yield 'd';
yield 'e';
}
for (const ch of alphabet()) {
console.log(ch); // a b c d e
}
yield*可將另一個可迭代物件的所有值「委派」出去,寫起來更簡潔。
範例 3:生成器控制非同步流程(舊版協程寫法)
function* asyncTask() {
const data1 = yield fetch('/api/step1').then(r => r.json());
console.log('step1 完成', data1);
const data2 = yield fetch('/api/step2', { method: 'POST', body: JSON.stringify(data1) })
.then(r => r.json());
console.log('step2 完成', data2);
}
// 協調器(runner)把 promise 解開
function run(gen) {
const iterator = gen();
function handle(result) {
if (result.done) return;
// result.value 可能是 Promise
Promise.resolve(result.value).then(
res => handle(iterator.next(res)),
err => iterator.throw(err)
);
}
handle(iterator.next());
}
run(asyncTask);
說明:在
async/await出現前,開發者常用此模式把yield與Promise結合,實現「同步寫法」的非同步流程。
範例 4:生成器與資料流(Node.js)
const { Readable } = require('stream');
function* numberStream(limit) {
for (let i = 1; i <= limit; i++) {
yield i;
}
}
const readable = Readable.from(numberStream(5));
readable.on('data', chunk => console.log('收到資料:', chunk));
readable.on('end', () => console.log('串流結束'));
Readable.from可以直接接受一個生成器,將其轉換為 Node.js 可讀串流。
範例 5:使用 finally 清理資源
function* resourceDemo() {
const resource = { open: true };
try {
yield '使用資源';
} finally {
resource.open = false; // 確保一定會執行
console.log('資源已釋放');
}
}
const r = resourceDemo();
console.log(r.next().value); // 使用資源
r.return(); // 直接結束,觸發 finally
常見陷阱與最佳實踐
| 陷阱 | 說明 | 建議的做法 |
|---|---|---|
忘記 yield |
生成器裡若只寫普通程式碼,呼叫 next() 時不會暫停,等同普通函式。 |
每個需要「產出」的地方必須使用 yield(或 yield*)。 |
yield 後的表達式未被執行 |
yield 會先回傳左側的值,右側的副作用在下一次 next() 時才會執行。 |
若有副作用,寫在 yield 前或使用 yield* 包裝。 |
在 async function* 中忘記 await |
直接 yield promise 會把 Promise 本身傳出去,而不是解開的值。 |
使用 yield await promise,或在外層 for await...of 解開。 |
過度依賴 return() 結束生成器 |
return 會立即結束,可能導致未執行 finally 之外的清理程式。 |
仍建議在 finally 中處理資源釋放,return 只作為提前終止的手段。 |
生成器與 this 綁定混淆 |
生成器本身是普通函式,this 取決於呼叫方式。 |
若需要保留 this,可使用箭頭函式包裝或 bind。 |
最佳實踐
- 保持單一職責:生成器應只負責「資料產出」或「流程控制」,不要混入過多業務邏輯。
- 使用
yield*代理子生成器:可讓程式結構更清晰,避免重複while/for迴圈。 - 在非同步場景使用
async function*:配合for await...of,讓資料流的讀寫更自然。 - 適當加入
try...finally:確保即使外部提前終止(return()),資源仍會被釋放。 - 避免在 UI 主執行緒中產生大量同步
yield:大量同步產出會阻塞渲染,必要時可將產出分批或使用setTimeout讓事件迴圈喘口氣。
實際應用場景
| 場景 | 為何適合使用生成器 | 範例 |
|---|---|---|
| 逐筆讀取大型檔案(Node.js) | 只在需要時讀取下一筆,減少記憶體占用 | fs.createReadStream + Readable.from(generator) |
| 分頁 API 呼叫 | 依次請求下一頁,使用 yield 暫停,讓呼叫者自行決定何時繼續 |
function* paginate(url) { … } |
| 遊戲或動畫的腳本 | 用 yield 表示「等待 X 秒」或「等待使用者輸入」的節點,使腳本更易寫 |
function* tutorial() { yield wait(2); … } |
| 協程式非同步流程(舊版瀏覽器) | 把多個 Promise 串成線性寫法,提升可讀性 |
如上「範例 3」的 run 協調器 |
| 資料流管線(如 RxJS) | 生成器本身即是一個可迭代的資料來源,可直接與 pipe、map 結合 |
function* numbers(){ … } → Array.from(numbers()) |
總結
生成器是 JavaScript 中一項強大的語言特性,透過 function*、yield 以及 for...of / for await...of 的結合,我們可以:
- 懶惰產出 任意長度或無窮的資料序列,降低記憶體壓力。
- 控制非同步流程,在
async function*中以同步的語意撰寫非同步程式。 - 建立可組合的迭代器,使用
yield*把子生成器委派出去,讓程式結構更模組化。 - 安全釋放資源,透過
try...finally或return()保證即使提前終止也能清理。
對於從前端 UI 互動、Node.js 後端串流,到大型資料處理與遊戲腳本,生成器都提供了 簡潔、可讀且高效 的解決方案。掌握這項技術,你的 JavaScript 程式碼將更具彈性、可維護性,並能在各種實務情境中發揮威力。祝你在寫程式的旅程中,玩得開心、寫得順手!