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() 直到 donetrue

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-handling03-async-printing

小結#

  • 迭代器協議:實作 next() 回傳 { value, done },即可被 for...of / 展開 / 解構消費。
  • function* + yield:可暫停、可恢復,保留完整執行環境。
  • 惰性求值讓無限序列成為可能。
  • next(value) 提供雙向通訊;yield* 委派其他可迭代物件。
  • Generator 是理解 async/await 的最佳跳板。