Generator 是 JavaScript 中一種「可以暫停與恢復」的函式。一般函式一旦開始執行,就會一路跑到 return 或結尾;Generator 卻能在 yield 處停下來,把控制權交還給呼叫者,之後再從停下的地方繼續。
這個特性讓 Generator 成為狀態機、惰性序列與非同步流程控制的利器。
迭代器協議#
要理解 Generator,得先認識它背後的「迭代器協議(Iterator Protocol)」。一個物件只要實作 next() 方法,每次回傳 { value, done },它就是一個迭代器。
// 手寫一個迭代器:產生 0、1、2
function createCounter() {
let i = 0;
return {
next() {
return i < 3
? { value: i++, done: false }
: { value: undefined, done: true };
},
};
}
const it = createCounter();
it.next(); // { value: 0, done: false }
it.next(); // { value: 1, done: false }
it.next(); // { value: 2, done: false }
it.next(); // { value: undefined, done: true }
for...of、展開運算子 ...、解構賦值都是建立在這個協議之上——它們會自動呼叫 next() 直到 done 為 true。
Generator 語法#
用 function* 宣告,用 yield 產出值。Generator 函式被呼叫時不會立即執行,而是回傳一個迭代器;每次呼叫 next() 才執行到下一個 yield。
function* counter() {
yield 0;
yield 1;
yield 2;
}
const it = counter();
it.next(); // { value: 0, done: false }
it.next(); // { value: 1, done: false }
// 因為實作了迭代器協議,可直接用 for...of
for (const n of counter()) {
console.log(n); // 0, 1, 2
}
// 也可展開
[...counter()]; // [0, 1, 2]
stateDiagram-v2
[*] --> 暫停: 呼叫 counter() 取得迭代器
暫停 --> 執行中: next()
執行中 --> 暫停: 遇到 yield 交還控制權
執行中 --> 結束: 抵達 return / 函式結尾
結束 --> [*]Generator 在
yield暫停時,完整保留了當下的執行環境(區域變數、執行位置)。這與一般函式回傳後就丟棄堆疊框架完全不同,也是它能「續跑」的根本原因。
惰性與無限序列#
因為值是「要的時候才算」,Generator 能表示無限序列而不會耗盡記憶體。
function* idMaker() {
let id = 1;
while (true) {
yield id++;
}
}
const ids = idMaker();
ids.next().value; // 1
ids.next().value; // 2
// 永遠不會「算完」,但只在需要時計算
雙向通訊:next 傳值進去#
yield 不只是「產出」,它也是一個運算式,next(value) 傳入的值會成為「上一個 yield 運算式的結果」。這讓呼叫者能在過程中「餵資料」給 Generator。
function* conversation() {
const name = yield "你叫什麼名字?";
const lang = yield `你好 ${name},你用什麼語言?`;
return `${name} 使用 ${lang}`;
}
const c = conversation();
c.next().value; // "你叫什麼名字?"(第一個 next 不傳值)
c.next("Andrew").value; // "你好 Andrew,你用什麼語言?"
c.next("JavaScript").value; // "Andrew 使用 JavaScript"
第一個
next()的參數會被忽略,因為此時還沒有「上一個yield」可以接收它。第一次呼叫只是把 Generator 推進到第一個yield。
yield* 委派#
yield* 可以把另一個可迭代物件「攤平」進來,常用於遞迴遍歷結構(如 DOM 樹)。
function* range(start, end) {
for (let i = start; i < end; i++) yield i;
}
function* combined() {
yield* range(0, 3); // 委派給 range
yield* [10, 11]; // 也能委派陣列
}
[...combined()]; // [0, 1, 2, 10, 11]
用 yield* 遞迴遍歷 DOM 樹
function* traverse(node) {
yield node;
for (const child of node.children) {
yield* traverse(child); // 遞迴委派
}
}
for (const el of traverse(document.body)) {
console.log(el.tagName);
}惰性遍歷的好處是:可以隨時 break 中止,不必先把整棵樹收集成陣列。
與非同步的淵源#
Generator 「暫停—恢復」的能力,正是 async/await 背後的概念原型:把非同步流程寫得像同步一樣。理解 Generator 後再看 async/await,會發現它本質上是「自動幫你呼叫 next() 的 Generator」。
// async/await 的心智模型:每個 await 就像一個會等 Promise 完成的 yield
async function load() {
const user = await fetchUser(); // 暫停,等 Promise resolve
const posts = await fetchPosts(user.id); // 再暫停
return posts;
}延伸閱讀:非同步處理的細節見 06-async-handling 與 03-async-printing。
小結#
- 迭代器協議:實作
next()回傳{ value, done },即可被for...of/ 展開 / 解構消費。 function*+yield:可暫停、可恢復,保留完整執行環境。- 惰性求值讓無限序列成為可能。
next(value)提供雙向通訊;yield*委派其他可迭代物件。- Generator 是理解
async/await的最佳跳板。