本文 AI 產出,尚未審核

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 } 形狀的結果。valueyield 產出的值,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. yieldyield*

  • 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 產生一個從 startend 的遞增序列,適合取代繁雜的 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 暫停,若未正確 returndone 會永遠是 false 在結束前 return 明確結束,或讓迴圈自然結束。
過度使用 Generator 在簡單場景使用 Generator 會增加認知負擔。 只在需要惰性求值自訂迭代非同步流程控制時使用。
錯誤傳遞不當 iterator.throw() 若未被捕獲會直接導致程式崩潰。 在 Generator 內使用 try…catch 包住可能拋錯的區塊。

最佳實踐

  1. 保持單一職責:每個 Generator 只負責一件事(例如產生序列、委派子序列),避免過度複雜。
  2. 使用 yield* 合併迭代:讓程式碼更清晰,避免手動迭代子迭代器。
  3. 配合 for...of:最自然的遍歷方式,讓迭代過程自動管理 done
  4. 在非同步環境使用執行器:如 coasync-generator 或自行實作 run,可將 Promise 與 Generator 結合,減少回呼地獄。
  5. 記得釋放資源:若 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 能以惰性求值自訂迭代非同步流程控制的方式編寫更乾淨、可維護的程式碼。掌握以下核心要點,即可在日常開發中靈活運用:

  1. 使用 function*yield 建立 可暫停 的函式。
  2. 透過 yield* 委派 其他可迭代物件,實作複雜的迭代邏輯。
  3. 利用 next(value)throw(error) 在迭代過程中雙向傳遞資料與錯誤。
  4. 結合 Promiseasync/await,將非同步操作寫成類似同步的流程。
  5. 在需要大量資料流分頁樹/圖遍歷副作用管理時,優先考慮 Generator。

只要在適當的情境下使用,Generator 能大幅減少手動迴圈與回呼的雜訊,提升程式的可讀性與效能。希望本篇文章能幫助你從概念到實作,快速上手 Generator,並在未來的專案中發揮它的最大價值。祝開發愉快! 🚀