本文 AI 產出,尚未審核

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 }
  • valueyield 的結果。
  • 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 出現前,開發者常用此模式把 yieldPromise 結合,實現「同步寫法」的非同步流程。

範例 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

最佳實踐

  1. 保持單一職責:生成器應只負責「資料產出」或「流程控制」,不要混入過多業務邏輯。
  2. 使用 yield* 代理子生成器:可讓程式結構更清晰,避免重複 while/for 迴圈。
  3. 在非同步場景使用 async function*:配合 for await...of,讓資料流的讀寫更自然。
  4. 適當加入 try...finally:確保即使外部提前終止(return()),資源仍會被釋放。
  5. 避免在 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) 生成器本身即是一個可迭代的資料來源,可直接與 pipemap 結合 function* numbers(){ … }Array.from(numbers())

總結

生成器是 JavaScript 中一項強大的語言特性,透過 function*yield 以及 for...of / for await...of 的結合,我們可以:

  • 懶惰產出 任意長度或無窮的資料序列,降低記憶體壓力。
  • 控制非同步流程,在 async function* 中以同步的語意撰寫非同步程式。
  • 建立可組合的迭代器,使用 yield* 把子生成器委派出去,讓程式結構更模組化。
  • 安全釋放資源,透過 try...finallyreturn() 保證即使提前終止也能清理。

對於從前端 UI 互動、Node.js 後端串流,到大型資料處理與遊戲腳本,生成器都提供了 簡潔、可讀且高效 的解決方案。掌握這項技術,你的 JavaScript 程式碼將更具彈性、可維護性,並能在各種實務情境中發揮威力。祝你在寫程式的旅程中,玩得開心、寫得順手!