寫程式時,你多半已經用過 for...of、展開運算子(...)、或是把東西丟進 Promise.all。這些看似日常的小語法,其實都靠一個共同的基礎:Iterator 與可迭代協定(Iterable)。
懂它們,等於把「資料一個一個被消費」這件事說清楚:什麼時候會產生下一個值、什麼時候該結束、遇到錯誤怎麼收尾。這篇文章不走玄學,只從實務出發:先把協定原理講清,再示範自訂迭代器與 Generator,最後帶你看非同步迭代(for await...of)在抓分頁、讀串流時的用法。
也會整理常見雷點,例如把 for...in 當 for...of、next() 回傳格式不對、或忽略 return() 造成資源沒釋放。讀完,你會把迭代當成工具,而不是黑箱。希望本篇文章能夠幫助到需要的您。
目錄
{tocify} $title={目錄}
為什麼你需要關心 Iterator?
Iterator 讓「資料一筆一筆被消費」成為語言一等公民。它的價值在於:
統一的存取介面:陣列、字串、Map/Set、TypedArray、DOM NodeList、甚至你自訂的資料結構,都能以相同方式走訪。
惰性(Lazy)處理:不必一次產出所有結果,能在需要時才計算(適合大資料、串流、分頁)。
語言整合:for...of、展開(...)、解構、yield*、Promise.allSettled 等都可直接吃「可迭代」。
兩個協定:Iterable 與 Iterator
1. Iterable(可迭代)
物件只要實作 obj[Symbol.iterator]() 並回傳 Iterator,就被視為 可迭代。常見內建可迭代:
Array、String、Map、Set、TypedArray
arguments、NodeList(多數現代瀏覽器)
Generator 物件本身
2. Iterator(迭代器)
迭代器是一個有 next() 方法的物件,每次呼叫回傳 { value, done }:
done: false 表示還有值,value 是當前資料。
done: true 表示結束;value 可省略。
可選的清理鉤子:
return():提早結束迭代時被呼叫(例如 break)。
throw(e):迭代器可選支援錯誤傳遞。
極簡範例:
const iterable = {
[Symbol.iterator]() {
let i = 0;
return {
next() {
if (i < 3) return { value: i++, done: false };
return { value: undefined, done: true };
}
};
}
};
for (const n of iterable) console.log(n); // 0, 1, 2
for...of、for...in、forEach 差在哪?
for...of:走 值(values),要求目標是 Iterable。尊重 Symbol.iterator。
for...in:走 可列舉鍵(enumerable keys),包含原型鏈上的可列舉屬性。與迭代器無關,不適合遍歷陣列元素。
Array.prototype.forEach:只適用陣列;不能 break/continue/return 提早中止,也不支援 await(同步呼叫 callback)。
結論:大多數情境用 for...of 更語意清楚、也更通用。
展開運算子、解構、Promise.all 與「可迭代」
展開(Spread):[...iterable]、new Set(iterable)、fn(...iterable)。
解構:const [first, second] = iterable;
多數語法與 API 都倚賴 Iterable:舉例 Array.from(iterable)、Promise.all(iterableOfPromises)(要求可迭代集合)。
小示例:
const set = new Set([1, 2, 3]);
const arr = [...set]; // [1, 2, 3]
const [a, b] = '你好'; // a = '你', b = '好'
自訂 Iterator:從入門到進階
1. 可重入的 Range
每次迭代都應產生 獨立 的迭代器,否則不同 for...of 會互相干擾。
function range(start, end, step = 1) {
return {
[Symbol.iterator]() {
let i = start;
return {
next() {
if (i < end) {
const v = i;
i += step;
return { value: v, done: false };
}
return { done: true };
},
// 可選:清理(當外界 break/throw 時被調用)
return() {
// 釋放資源、關閉連線...(視需求)
return { done: true };
}
};
}
};
}
console.log([...range(0, 5)]); // [0,1,2,3,4]
2. 單次迭代 vs 可重複迭代
若 obj[Symbol.iterator] 回傳 this,且 next() 狀態存在於 this,那它通常是 一次性 的(迭完即壞)。多數情況建議每次回傳 新物件,以達到可重入。
3. 進階:樹的 DFS 迭代器
function makeTreeDFS(root) {
return {
[Symbol.iterator]() {
const stack = [root];
return {
next() {
if (stack.length === 0) return { done: true };
const node = stack.pop();
// 後推入的會先被彈出 => DFS
if (node.children) {
for (let i = node.children.length - 1; i >= 0; i--) {
stack.push(node.children[i]);
}
}
return { value: node, done: false };
}
};
}
};
}
// 用法
const tree = { value: 'A', children: [{ value: 'B' }, { value: 'C', children: [{ value: 'D' }] }] };
for (const node of makeTreeDFS(tree)) console.log(node.value); // A B C D
Generator 與 Iterator 的關係
Generator(產生器) 是一種更好寫的迭代器工廠:function* 會回傳 同時具備 Iterator 與 Iterable 的物件。yield 用來逐步產生值;yield* 可委派到另一個可迭代。
function* fib(limit = 10) {
let a = 0, b = 1;
while (limit-- > 0) {
yield a;
[a, b] = [b, a + b];
}
}
console.log([...fib(5)]); // [0,1,1,2,3]
雙向溝通:
function* ask() {
const name = yield "你的名字?";
yield `Hello, ${name}`;
}
const g = ask();
console.log(g.next().value); // "你的名字?"
console.log(g.next("JS").value); // "Hello, JS"
清理與錯誤傳遞:
function* task() {
try {
yield 1;
yield 2;
} finally {
console.log('cleanup');
}
}
const it = task();
console.log(it.next()); // {value:1, done:false}
console.log(it.return()); // 觸發 finally,並結束
非同步迭代:AsyncIterator 與 for await...of
當資料逐批、非同步到達(如 API 分頁、檔案串流),用 Async Iterator 最自然。實作 obj[Symbol.asyncIterator](),迭代時搭配 for await...of。
const api = {
async *[Symbol.asyncIterator]() {
let page = 1;
while (page <= 3) {
const data = await fetch(`/v1/items?page=${page}`).then(r => r.json());
if (data.items.length === 0) return; // 沒資料就結束
yield data.items;
page++;
}
}
};
(async () => {
for await (const batch of api) {
console.log('批次資料', batch);
}
})();
Node.js 也把串流與 Async Iterator 打通,例如 Readable.from(iterable),或對 stream.Readable 使用 for await...of 來讀資料。
惰性運算工具:以迭代組出 map / filter / take / drop / chunk
用 Iterables 可做「不創建中間大陣列」的資料管線。
const mapIter = (iterable, fn) => ({
[Symbol.iterator]() {
const it = iterable[Symbol.iterator]();
return {
next() {
const { value, done } = it.next();
if (done) return { done: true };
return { value: fn(value), done: false };
},
return() { return it.return ? it.return() : { done: true }; }
};
}
});
const filterIter = (iterable, pred) => ({
[Symbol.iterator]() {
const it = iterable[Symbol.iterator]();
return {
next() {
while (true) {
const { value, done } = it.next();
if (done) return { done: true };
if (pred(value)) return { value, done: false };
}
},
return() { return it.return ? it.return() : { done: true }; }
};
}
});
const take = (iterable, n) => ({
[Symbol.iterator]() {
const it = iterable[Symbol.iterator]();
let remaining = n;
return {
next() {
if (remaining-- <= 0) return { done: true };
return it.next();
},
return() { return it.return ? it.return() : { done: true }; }
};
}
});
// 用法:只取平方後的前三個偶數
const src = [1,2,3,4,5,6];
const pipe = take(mapIter(filterIter(src, x => x % 2 === 0), x => x * x), 3);
console.log([...pipe]); // [4, 16, 36]
效能、記憶體與測試除錯心法
惰性優勢:不建立中間陣列,逐筆計算,對大資料友善。
短路(short-circuit):配合 take(n)、break 可提早停止,避免多餘工作。
測試:把迭代器「抽離資料來源」,以假資料(小集合)驗證 next() 行為;測 done 邏輯尤其重要。
清理:支援 return()/finally,確保出錯或提前結束時釋放資源。
常見錯誤與雷點
1. 把 for...in 當 for...of 用
for (const k in [10, 20]) console.log(k); // "0","1"(鍵)不是值 for (const v of [10, 20]) console.log(v); // 10, 20(值)
2. 「不是可迭代」導致 TypeError: obj is not iterable
const obj = { a: 1, b: 2 };
// [...obj] // TypeError
const iterableObj = {
data: obj,
[Symbol.iterator]() {
const entries = Object.entries(this.data);
let i = 0;
return {
next() {
if (i < entries.length) return { value: entries[i++], done: false };
return { done: true };
}
};
}
};
console.log([...iterableObj]); // [ ['a',1], ['b',2] ]
3. 實作 next() 回傳格式錯誤
next() 必須回傳 物件,且含 done 布林欄位,否則報錯或行為不定。
4. 覆寫 Symbol.iterator 卻回傳 this
若迭代狀態存在於 this,多次迭代會競爭同一指標,導致結果錯亂。請回傳 新 迭代器物件。
5. 迭代時修改集合
對 Array/Map/Set 邊走訪邊新增/刪除,結果可能不如預期(有時跳過元素、有時重複)。建議:先複製快照或收集要處理的鍵再迭代。
const set = new Set([1,2,3]);
for (const v of set) {
if (v === 2) set.add(4); // 4 是否被訪問取決於實作時機,避免這樣做
}
6. forEach 不支援 break/continue、也不搭配 await
// 錯誤期待:這不會等待每個 async callback
[1,2,3].forEach(async (x) => {
await new Promise(r => setTimeout(r, 100));
console.log(x);
});
// 正解
for (const x of [1,2,3]) {
await new Promise(r => setTimeout(r, 100));
console.log(x);
}
7. 忽略 return() 導致資源未釋放
自訂迭代器若握有資源(檔案、連線),沒實作 return(),用戶在中途 break 時無法清理。
8. 字串與 Unicode 代理對(Surrogate Pairs)
用索引走字串會切裂 emoji;用 for...of 就安全(以 Code Point 迭代)。
const s = 'A😊B'; console.log([...s]); // ['A','😊','B'] console.log(s[1]); // 可能是半個代理對,錯
9. Async Iterator 忘記 try/finally
for await...of 中若拋錯,迭代器端應以 finally 做收尾。
實戰:可中止的「分頁 API 抓取器」(Async Iterator + AbortController)
場景:你要逐頁從後端抓資料,但想要:
邊抓邊用(不用等全部結束)
隨時可取消(例如使用者切換頁面)
function createPagedFetcher(fetchPage) {
return function paged({ from = 1, to = Infinity, signal } = {}) {
return {
async *[Symbol.asyncIterator]() {
const controller = new AbortController();
const composite = signal
? new AbortSignal.any([signal, controller.signal])
: controller.signal;
try {
for (let page = from; page <= to; page++) {
const data = await fetchPage(page, { signal: composite });
if (!data || data.items?.length === 0) return;
yield data.items;
}
} finally {
// 確保中止底層請求或釋放資源
controller.abort();
}
}
};
};
}
// ===== 使用範例 =====
async function fetchPage(page, { signal }) {
const res = await fetch(`/api/items?page=${page}`, { signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
const paged = createPagedFetcher(fetchPage);
const ac = new AbortController();
(async () => {
try {
let count = 0;
for await (const items of paged({ from: 1, to: 100, signal: ac.signal })) {
console.log('拿到批次', ++count, items.length);
if (count === 3) ac.abort(); // 模擬用戶中止
}
} catch (e) {
if (e.name === 'AbortError') {
console.log('已取消');
} else {
console.error('發生錯誤', e);
}
}
})();
重點:
以 Async Iterator 暴露逐批資料。
以 AbortController 實作可取消。
在 finally 中確保釋放(避免資源洩漏)。
相容性
相容性:ES2015 起,現代瀏覽器與 Node.js 均支援 Iterator/Iterable;for await...of 與 Symbol.asyncIterator 需較新版本(Node 10+,現代瀏覽器)。舊環境可考慮 Babel/TypeScript 轉譯。
Polyfill:若要在非常舊環境支援 Symbol 與迭代協定,請使用官方 polyfill(如 core-js)並確認體積與授權。
問題集
Q1. 我要遍歷一般物件?
用 Object.entries(obj) 取得可迭代陣列,或自訂 Symbol.iterator。
Q2. 何時用 Generator、何時手寫迭代器?
大多數情況用 Generator 可讀性更高;手寫適合需要細膩控制 next/return/throw 或避免引入 function* 的場景。
Q3. Async Iterator 與 RxJS 差異?
Async Iterator 強調 拉(pull) 模式、單消費者;RxJS 強調 推(push)、多播、操作子豐富。串流事件、背壓控制需求重時,RxJS 可能更合適。
附錄:更多實用小範例
1. 用 yield* 委派迭代
function* combine(...iterables) {
for (const it of iterables) {
yield* it;
}
}
console.log([...combine([1,2], new Set([3,4]), '56')]); // [1,2,3,4,'5','6']
2. 以 Generator 寫 chunk(分塊)
function* chunk(iterable, size) {
let buf = [];
for (const x of iterable) {
buf.push(x);
if (buf.length === size) {
yield buf;
buf = [];
}
}
if (buf.length) yield buf;
}
console.log([...chunk([1,2,3,4,5], 2)]); // [[1,2],[3,4],[5]]
3. 以 Async Generator 包裝檔案逐行讀取(Node.js)
import { createReadStream } from 'node:fs';
import readline from 'node:readline';
async function* readLines(path) {
const rl = readline.createInterface({
input: createReadStream(path),
crlfDelay: Infinity
});
try {
for await (const line of rl) yield line;
} finally {
rl.close(); // 保證清理
}
}
延伸閱讀推薦:
javaScript設計模式 : Factory Method (工廠方法)
javaScript設計模式 : Abstract Factory(抽象工廠)
javaScript設計模式 : Builder(建造者模式)
javaScript設計模式 : Prototype(原型)
javaScript設計模式 : Singleton (單例模式)
javaScript設計模式 : Adapter(轉接器模式)
javaScript設計模式 : Bridge( 橋接模式 )
javaScript設計模式 : Composite(組合模式)
javaScript設計模式 : Decorator(裝飾者)
javaScript設計模式 : Flyweight(享元模式)
javaScript設計模式 : Chain of Responsibility(責任鏈)
javaScript設計模式 : Command Pattern(命令模式)
javaScript設計模式 : Iterator(迭代器)
javaScript設計模式 : Mediator(仲裁者)
javaScript設計模式 : Observer( 觀察者 )
javaScript設計模式 : Strategy(策略模式)
