JavaScript ES6+ 新特性:Generator 函式
簡介
在 ECMAScript 6(ES6)之後,JavaScript 加入了許多讓程式碼更簡潔、可讀且可維護的語法特性,其中 Generator(產生器)是最具革命性的功能之一。Generator 讓我們可以暫停與恢復函式的執行,天然支援惰性求值(lazy evaluation),在處理大量資料、非同步流程或是實作自訂的迭代器時,提供了前所未有的彈性。
對於剛踏入 JavaScript 的新手而言,Generator 可能看起來有些抽象;但只要掌握背後的概念與常見用法,就能在實務開發中減少大量的迴圈與回呼(callback)程式碼,提升程式的可讀性與效能。本文將從 核心概念、實作範例、常見陷阱與最佳實踐,一步步帶你深入了解 Generator,並提供實際的應用場景,讓你能在專案中立即上手。
核心概念
1. 什麼是 Generator?
Generator 是一種特殊的函式,使用 function*(星號)語法宣告。它會回傳一個 迭代器物件(iterator),而不是立即執行函式本體。透過 yield 關鍵字,我們可以在函式內暫停執行,並把當前的值「產出」給呼叫端;之後再以 .next() 方法繼續執行,直到遇到下一個 yield 或函式結束。
function* simpleGenerator() {
console.log('開始執行');
yield 1; // 暫停,回傳 1
console.log('繼續執行');
yield 2; // 暫停,回傳 2
console.log('結束');
}
重點:
yield只在 Generator 函式內有效;普通函式若使用yield會拋出語法錯誤。
2. Iterator 與 Iterable
- Iterator:擁有
next()方法的物件,呼叫next()會得到{ value, done }形狀的結果。value是yield產出的值,done為布林值,表示是否已遍歷完畢。 - Iterable:實作了
[Symbol.iterator]方法的物件,讓它能被for...of、擴充運算子(spread operator)等語法自動迭代。
Generator 本身即是一個 Iterable,因為它的返回值(迭代器)已經實作了 [Symbol.iterator]。
const gen = simpleGenerator(); // gen 為 iterator
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: undefined, done: true }
3. yield 與 yield*
yield:單純產出一個值,可接收呼叫端傳入的參數(next(value))。yield*:委派(delegate)給另一個可迭代物件(如另一個 Generator、陣列、字串等),讓外層 Generator 直接遍歷內層的值。
function* inner() {
yield 'A';
yield 'B';
}
function* outer() {
yield 1;
yield* inner(); // 委派給 inner
yield 2;
}
4. 接收外部輸入:next(value)
next() 除了控制執行流程外,還能把外部資料傳回 Generator,成為 yield 表達式的返回值。
function* echo() {
const input = yield '請輸入文字:';
console.log('收到:', input);
}
const it = echo();
console.log(it.next().value); // 輸出提示文字
it.next('Hello Generator!'); // 送入字串,Generator 內部接收
程式碼範例
以下提供 5 個實用範例,涵蓋基礎、委派、惰性序列、非同步與錯誤處理等情境。
範例 1:基本迭代器與 for...of
function* range(start, end, step = 1) {
for (let i = start; i <= end; i += step) {
yield i; // 每次產出一個數字
}
}
// 使用 for...of 直接遍歷
for (const num of range(1, 5)) {
console.log(num); // 1 2 3 4 5
}
說明:
range產生一個從start到end的遞增序列,適合取代繁雜的for迴圈。
範例 2:yield* 委派多層迭代
function* alphabets() {
yield* ['A', 'B', 'C']; // 直接委派陣列
}
function* numbers() {
yield* [1, 2, 3];
}
function* combined() {
yield* alphabets(); // 先產出字母
yield* numbers(); // 再產出數字
}
console.log([...combined()]); // ['A','B','C',1,2,3]
說明:利用
yield*可以把多個來源(陣列、Generator)合併成單一迭代流,程式碼更具可讀性。
範例 3:惰性資料流(Lazy Evaluation)— 無限斐波那契數列
function* fibonacci() {
let a = 0, b = 1;
while (true) { // 無限迴圈,依需求產生
yield a;
[a, b] = [b, a + b]; // ES6 解構賦值
}
}
// 只取前 10 個
const fib = fibonacci();
for (let i = 0; i < 10; i++) {
console.log(fib.next().value); // 0 1 1 2 3 5 8 13 21 34
}
重點:Generator 天生支援惰性求值,大幅減少記憶體佔用,特別適合處理無限或巨量資料。
範例 4:非同步流程控制(配合 async/await)
雖然 Generator 本身是同步的,但可以與 Promise 結合,實作類似 async/await 的流程控制(在舊版瀏覽器或 Node.js 6 前常見)。
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 用 Generator 包裝非同步流程
function* task() {
console.log('開始任務');
yield delay(1000); // 暫停 1 秒
console.log('1 秒後');
const data = yield fetch('https://api.example.com/data')
.then(res => res.json());
console.log('取得資料:', data);
}
// 執行器(runner)負責自動迭代 Promise
function run(gen) {
const iterator = gen();
function step(nextF) {
const { value, done } = nextF();
if (done) return;
if (value instanceof Promise) {
value.then(res => step(() => iterator.next(res)));
} else {
step(() => iterator.next(value));
}
}
step(() => iterator.next());
}
// 呼叫
run(task);
說明:
run函式會自動等待每個yield回傳的 Promise 完成,再把結果送回 Generator,讓程式看起來像同步流程。
範例 5:錯誤傳遞與捕獲
Generator 允許使用 throw() 方法把錯誤拋入迭代器,讓 try…catch 在 Generator 內部捕獲。
function* safeDivisor() {
try {
const divisor = yield '請輸入除數:';
if (divisor === 0) throw new Error('除數不能為 0');
yield 100 / divisor;
} catch (err) {
yield `錯誤:${err.message}`;
}
}
const it = safeDivisor();
console.log(it.next().value); // 提示文字
console.log(it.throw(new Error('測試錯誤')).value); // 捕獲並回傳錯誤訊息
重點:透過
iterator.throw(error)可以在 Generator 中主動拋錯,讓錯誤處理更集中。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
忘記使用 function* |
使用 yield 卻未以 function* 宣告會拋語法錯誤。 |
確認星號位置:function* name() {} |
yield 只能在 Generator 中 |
在普通函式或箭頭函式內使用 yield 會失敗。 |
若需要在箭頭函式內使用,必須先宣告外層 Generator,或改寫為 function*。 |
| 迭代器未被消耗 | 呼叫 generator() 後未執行 .next(),函式本體不會執行。 |
使用 for...of、展開運算子 ... 或手動 .next()。 |
忘記 return 結束迭代 |
Generator 只靠 yield 暫停,若未正確 return,done 會永遠是 false。 |
在結束前 return 明確結束,或讓迴圈自然結束。 |
| 過度使用 Generator | 在簡單場景使用 Generator 會增加認知負擔。 | 只在需要惰性求值、自訂迭代或非同步流程控制時使用。 |
| 錯誤傳遞不當 | iterator.throw() 若未被捕獲會直接導致程式崩潰。 |
在 Generator 內使用 try…catch 包住可能拋錯的區塊。 |
最佳實踐
- 保持單一職責:每個 Generator 只負責一件事(例如產生序列、委派子序列),避免過度複雜。
- 使用
yield*合併迭代:讓程式碼更清晰,避免手動迭代子迭代器。 - 配合
for...of:最自然的遍歷方式,讓迭代過程自動管理done。 - 在非同步環境使用執行器:如
co、async-generator或自行實作run,可將 Promise 與 Generator 結合,減少回呼地獄。 - 記得釋放資源:若 Generator 持有外部資源(檔案、網路連線),可在
finally區塊中清理,或使用return()方法手動提前結束。
實際應用場景
| 場景 | 為何選擇 Generator |
|---|---|
| 大型資料流(如 CSV、日誌) | 惰性讀取每一筆資料,避免一次載入全部佔用記憶體。 |
| 分頁 API 呼叫 | 以 yield 產生每一頁的結果,使用者只在需要時才發起下一次請求。 |
| 自訂迭代資料結構(樹、圖) | 用遞迴 Generator 實作深度優先或廣度優先遍歷,程式碼比手寫堆疊更簡潔。 |
| 非同步工作排程 | 結合 Promise,將多個非同步步驟串接成類似同步的流程,提升可讀性。 |
| 測試資料產生器 | 產生隨機或序列化的測試資料,測試框架可透過 for...of 直接取得。 |
| 實作 Redux-saga | Redux-saga 正是以 Generator 為核心,管理副作用(side‑effects)與非同步流程。 |
範例:分頁 API
async function* fetchPages(url) { let page = 1; while (true) { const res = await fetch(`${url}?page=${page}`); const data = await res.json(); if (data.items.length === 0) break; // 沒有資料就結束 yield data.items; // 產出本頁結果 page++; } } // 使用方式 for await (const items of fetchPages('/api/products')) { console.log('本頁商品數量:', items.length); }透過
for await...of(ES2018)即可直接遍歷非同步產出的資料序列,程式碼看起來就像同步迴圈。
總結
Generator 是 ES6+ 中極具威力的語法特性,讓 JavaScript 能以惰性求值、自訂迭代與非同步流程控制的方式編寫更乾淨、可維護的程式碼。掌握以下核心要點,即可在日常開發中靈活運用:
- 使用
function*與yield建立 可暫停 的函式。 - 透過
yield*委派 其他可迭代物件,實作複雜的迭代邏輯。 - 利用
next(value)與throw(error)在迭代過程中雙向傳遞資料與錯誤。 - 結合 Promise 或 async/await,將非同步操作寫成類似同步的流程。
- 在需要大量資料流、分頁、樹/圖遍歷或副作用管理時,優先考慮 Generator。
只要在適當的情境下使用,Generator 能大幅減少手動迴圈與回呼的雜訊,提升程式的可讀性與效能。希望本篇文章能幫助你從概念到實作,快速上手 Generator,並在未來的專案中發揮它的最大價值。祝開發愉快! 🚀