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.from、Promise.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 function(function*)自動實作 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.iterator,for...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...of 或 while 迴圈。 |
最佳實踐
- 優先使用 generator:如果可以用
function*表達,語法更簡潔且自動符合 iterator 協議。 - 保持迭代的純粹性:iterator 應只負責產生值,不應在
next中做副作用(例如修改外部狀態),除非是設計上需要的「惰性」計算。 - 遵守可迭代協議:若你的物件同時支援
keys()、values()、entries(),建議也提供相對應的 iterator,提升 API 一致性。 - 使用
for...of替代傳統for:for...of讓程式碼更具可讀性,同時自動處理 iterator 的return。 - 測試迭代結束情況:寫單元測試時,確保
done在最後一次正確返回true,避免無限迴圈。
實際應用場景
資料流處理(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); } })();分頁 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); } })();自訂集合類別
在大型前端專案中,常會自行實作像Set、Queue、PriorityQueue等資料結構。只要實作[Symbol.iterator],即可直接使用Array.from、spread、for...of等語法,讓資料結構更易整合。懶惰計算與無窮序列
數學或圖形程式常需要產生無限序列(例如隨機數、噪聲、帧率資料)。利用 iterator 的懶評估特性,我們可以在需要時才產生下一筆資料,節省記憶體與運算資源。跨平台資料序列化
某些序列化庫(如JSON.stringify)接受 iterable 作為輸入,讓我們可以把自訂資料結構直接轉成 JSON 陣列,而不必先手動Array.from。
總結
- iterable 與 iterator 為 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 後端,或是任何需要遍歷的資料結構,都能輕鬆運用這套機制,讓開發體驗更上一層樓。祝你玩得開心,寫程式更順手!