本文 AI 產出,尚未審核

ES6+ 新特性:可迭代物件與 Iterator

簡介

在 ECMAScript 2015(簡稱 ES6)之後,JavaScript 變得更像「全功能」的程式語言。**可迭代物件(iterable)**與 Iterator 正是其中最具影響力的概念之一。它們讓我們能以統一的方式遍歷陣列、字串、Map、Set,甚至自訂資料結構,從而寫出更簡潔、可讀性更高的程式碼。

在日常開發中,你可能已經在使用 for...of、展開運算子 (...)、Array.from() 等語法,卻不曉得背後其實是依賴 iterator protocol 以及 iterable protocol。了解這兩個協議不僅能幫助你掌握語法本身,更能讓你自訂「可遍歷」的資料結構,提升程式的彈性與可組合性。

本文將從概念說明、實作範例、常見陷阱與最佳實踐,最後帶你看看實務上如何運用 iterator,幫助你在 JavaScript 生態系中更游刃有餘。


核心概念

1. 什麼是 iterable?

iterable 是指具備 Symbol.iterator 屬性的物件。當 JavaScript 嘗試遍歷一個物件(例如 for...of、展開運算子)時,會呼叫這個屬性取得一個 iterator

// 內建的 iterable 範例:Array、String、Map、Set
const arr = [1, 2, 3];
console.log(typeof arr[Symbol.iterator]); // function

2. Iterator 是什麼?

一個 iterator 必須是一個物件,且必須實作 next() 方法。每次呼叫 next(),會回傳形如 { value: any, done: boolean } 的結果:

  • value:本次迭代產生的值
  • done:布林值,若為 true 表示迭代已結束
const iterator = arr[Symbol.iterator](); // 取得 iterator
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: undefined, done: true }

3. 為什麼需要 iterator?

  • 統一遍歷介面:不論是陣列、字串還是自訂物件,都可以用相同的語法遍歷。
  • 懶評估(lazy evaluation):iterator 可以在需要時才產生值,適合處理大量資料或無窮序列。
  • 可組合性:許多 ES6+ 的 API(如 Array.fromPromise.all)都接受 iterable,讓資料流的組合變得簡單。

4. 建立自訂 iterable

範例 1:簡易的倒數計數器

下面的例子展示如何把一個普通物件變成 iterable,讓它可以被 for...of 直接使用。

const countdown = {
  start: 5,
  // 必須實作 Symbol.iterator,回傳一個 iterator
  [Symbol.iterator]() {
    let current = this.start;
    return {
      // 每次呼叫 next,回傳當前值並遞減
      next() {
        if (current > 0) {
          return { value: current--, done: false };
        }
        // 結束迭代
        return { value: undefined, done: true };
      }
    };
  }
};

for (const num of countdown) {
  console.log(num); // 5 4 3 2 1
}

重點[Symbol.iterator] 必須回傳一個擁有 next 方法的物件,且 next 必須回傳 {value, done}

範例 2:使用 generator 簡化 iterator 實作

ES6 引入的 generator functionfunction*)自動實作 iterator,讓自訂 iterable 更簡潔。

const range = {
  from: 1,
  to: 5,
  // 直接回傳 generator,即可成為 iterator
  *[Symbol.iterator]() {
    for (let i = this.from; i <= this.to; i++) {
      yield i; // yield 會自動產生 {value, done}
    }
  }
};

console.log([...range]); // [1, 2, 3, 4, 5]

generator 的好處是語法簡潔、支援 yield* 直接委派其他 iterable,讓複雜的遍歷邏輯更易維護。

範例 3:無窮斐波那契序列(懶評估)

利用 generator,我們可以建立一個無窮的 iterator,只有在真的需要時才計算下一個值。

function* fibonacci() {
  let a = 0, b = 1;
  while (true) {
    yield a;
    [a, b] = [b, a + b]; // 同時更新 a, b
  }
}

// 只取前 10 個
const fib10 = Array.from({ length: 10 }, (_, i) => fibonacci().next().value);
console.log(fib10); // [0,1,1,2,3,5,8,13,21,34]

// 或者直接用 for...of 搭配 break
let count = 0;
for (const n of fibonacci()) {
  console.log(n);
  if (++count === 10) break; // 手動停止
}

範例 4:自訂資料結構 – LinkedList

下面示範如何為自訂的單向鏈結串列實作 iterator,讓它可以像陣列一樣使用 for...of

class ListNode {
  constructor(value, next = null) {
    this.value = value;
    this.next = next;
  }
}

class LinkedList {
  constructor() {
    this.head = null;
  }

  // 新增節點到尾端
  push(value) {
    if (!this.head) {
      this.head = new ListNode(value);
    } else {
      let cur = this.head;
      while (cur.next) cur = cur.next;
      cur.next = new ListNode(value);
    }
  }

  // 讓 LinkedList 成為 iterable
  *[Symbol.iterator]() {
    let cur = this.head;
    while (cur) {
      yield cur.value;
      cur = cur.next;
    }
  }
}

// 測試
const list = new LinkedList();
list.push(10);
list.push(20);
list.push(30);

for (const v of list) console.log(v); // 10 20 30
console.log([...list]); // [10,20,30]

範例 5:使用 iterator 手動遍歷 Map

有時候你需要直接操作 iterator(例如同時取得鍵和值),以下示範如何從 Map 取得 iterator 並自行迭代。

const map = new Map([
  ['apple', 1],
  ['banana', 2],
  ['cherry', 3]
]);

const iterator = map[Symbol.iterator](); // 同時回傳 [key, value] 陣列

let result = iterator.next();
while (!result.done) {
  const [key, value] = result.value;
  console.log(`${key} => ${value}`);
  result = iterator.next();
}
// apple => 1
// banana => 2
// cherry => 3

常見陷阱與最佳實踐

陷阱 說明 改善方式
忘記實作 Symbol.iterator 物件即使有 next,若缺 Symbol.iteratorfor...of 仍無法使用。 確保 每個 自訂 iterable 都定義 [Symbol.iterator](),即使只是回傳 this(若本身已是 iterator)。
next 回傳不完整的物件 只回傳值或缺 done,會導致迭代無法正確結束。 必須回傳 { value: ..., done: boolean }done 必須在最後一次迭代時設為 true
在 iterator 中拋出錯誤 next() 內部拋錯,外層 for...of 會直接中斷,且不會自動呼叫 return 使用 try...finally 包住迭代邏輯,或在 generator 中使用 try...catch 處理例外。
忘記 return 方法 某些語法(如 for...of 中使用 break)會呼叫 iterator 的 return,若未實作可能導致資源未釋放。 為自訂 iterator 實作 return(),在此釋放資源(例如關閉檔案、清除計時器)。
過度依賴 Array.from 產生陣列 大量資料使用 Array.from 會一次性載入記憶體,失去 iterator 的懶評估優勢。 只在需要完整陣列時使用;若可逐筆處理,直接使用 for...ofwhile 迴圈。

最佳實踐

  1. 優先使用 generator:如果可以用 function* 表達,語法更簡潔且自動符合 iterator 協議。
  2. 保持迭代的純粹性:iterator 應只負責產生值,不應在 next 中做副作用(例如修改外部狀態),除非是設計上需要的「惰性」計算。
  3. 遵守可迭代協議:若你的物件同時支援 keys()values()entries(),建議也提供相對應的 iterator,提升 API 一致性。
  4. 使用 for...of 替代傳統 forfor...of 讓程式碼更具可讀性,同時自動處理 iterator 的 return
  5. 測試迭代結束情況:寫單元測試時,確保 done 在最後一次正確返回 true,避免無限迴圈。

實際應用場景

  1. 資料流處理(Streams)
    在 Node.js 中,Readable 介面本身就是 iterable,允許使用 for await...of 直接讀取檔案或網路串流,結合 async iterator 可寫出簡潔的非同步程式碼。

    const fs = require('fs');
    const stream = fs.createReadStream('bigfile.txt', { encoding: 'utf8' });
    
    (async () => {
      for await (const chunk of stream) {
        console.log('收到片段:', chunk.length);
      }
    })();
    
  2. 分頁 API 呼叫
    當後端提供分頁結果時,我們可以把每一次的 API 呼叫封裝成一個 async iterator,使用者只需要 for await...of 即可取得完整資料。

    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++;
      }
    }
    
    (async () => {
      for await (const item of fetchPages('/api/products')) {
        console.log(item.id);
      }
    })();
    
  3. 自訂集合類別
    在大型前端專案中,常會自行實作像 SetQueuePriorityQueue 等資料結構。只要實作 [Symbol.iterator],即可直接使用 Array.fromspreadfor...of 等語法,讓資料結構更易整合。

  4. 懶惰計算與無窮序列
    數學或圖形程式常需要產生無限序列(例如隨機數、噪聲、帧率資料)。利用 iterator 的懶評估特性,我們可以在需要時才產生下一筆資料,節省記憶體與運算資源。

  5. 跨平台資料序列化
    某些序列化庫(如 JSON.stringify)接受 iterable 作為輸入,讓我們可以把自訂資料結構直接轉成 JSON 陣列,而不必先手動 Array.from


總結

  • iterableiterator 為 ES6+ 的核心概念,提供統一且可擴充的遍歷機制。
  • 只要物件擁有 Symbol.iterator,就能被 for...of、展開運算子、Array.from 等語法使用。
  • iterator 必須實作 next(),回傳 {value, done},而 generator function (function*) 能自動完成這套協議,讓自訂 iterable 更簡潔。
  • 了解 iterator 的懶評估特性,可在大量或無限資料處理上取得顯著效能提升。
  • 實務上,iterator 常見於 資料流、分頁 API、客製集合、無窮序列 等場景,配合 for await...of 更能處理非同步資料。

掌握了可迭代物件與 iterator,你就能在 JavaScript 生態中寫出更具彈性、可組合且易維護的程式碼。未來不論是前端 UI、Node.js 後端,或是任何需要遍歷的資料結構,都能輕鬆運用這套機制,讓開發體驗更上一層樓。祝你玩得開心,寫程式更順手!