for...of 從 Day 02 的 iterable 取得 iterator 後,會一次呼叫一次 next(); 如果我們用 break 提早離開,iterator 內部會收到中斷流程通知。[Symbol.iterator](),iterator 提供 next()
for...of 如何消費 iterator 與提前停止ES2015 for...of IteratorResult IteratorClose
平常寫 for...of 時,迴圈變數會在每一輪自動拿到資料:
for (const task of ["整理需求", "撰寫內容"]) {
console.log(task);
}
但 task 是怎麼得到不同值的?
上次已經理解到 for...of 如何從 iterable 的 [Symbol.iterator]() 取得 iterator。
今天要接著追蹤 for...of 如何向它要下一筆、把回傳結果帶進迴圈本體;以及迴圈用 break 提早停止時,為何還需要通知來源收尾結束。

TC39 是制定 ECMAScript(JavaScript 語言標準)的技術委員會。
GetIterator、IteratorResult、IteratorClose 是規格用來描述引擎行為的抽象操作名稱;它們不是開發者要手寫的 JavaScript 程式碼,也不是執行時會原封不動出現的函式呼叫。
它們的用途是讓不同 JavaScript 引擎對 for...of 實作出一致的語言行為,就好像開發一段需求時我們需要知道它如何啟動和結束。
先把這篇的心智地圖放在眼前。這些名稱不是另一套規則,而是 TC39 規格如何描述同一個 for...of 行為;接下來再逐步把它們展開:
| 讀者看到的行為 | 對應的規格概念 | 這篇要觀察的事 |
|---|---|---|
| 開始讀取 | GetIterator | 來源能否提供可呼叫的 [Symbol.iterator](),並交出 iterator。 |
| 要下一筆 | iterator 的 next() 與 IteratorResult |
每次交付的 { value, done },如何把 value 帶進迴圈變數。 |
| 自然讀完 | done: true |
iterator 如何表示已沒有下一筆,讓迴圈停止。 |
| 還沒讀完就離開 | IteratorClose | break 如何讓 for...of 嘗試通知來源收尾。 |
for...of 怎麼把每一筆帶進迴圈?把上面的 for...of 想成這條流程即可。這是為了理解而寫的展開,不是 JavaScript 規格的逐字轉寫:
const tasks = ["整理需求", "撰寫內容"];
const iterator = tasks[Symbol.iterator]();
while (true) {
const result = iterator.next();
if (result.done) break;
const task = result.value;
console.log(task);
}
每次 next() 都交付一個 { value, done }。done 還不是 true 時,for...of 才把 value 帶進該輪迴圈;概念上就像 const task = result.value。因此它不是先收集所有資料,而是「要一筆,才拿一筆」。
這也說明了 for...of 為什麼不能接受任意物件:它必須先取得能提供 next() 的 iterator。只有索引與 length、卻沒有可呼叫 [Symbol.iterator]() 的物件:
const taskLike = { 0: "整理需求", length: 1 };
// for (const task of taskLike) {
// console.log(task);
// }
// TypeError: taskLike is not iterable
錯誤不是因為 taskLike 沒有資料,而是 for...of 找不到可呼叫的 [Symbol.iterator](),無法取得 iterator。接下來用可追蹤的來源,讓這些原本看不見的互動實際印出來,這也是 TC39 中看到的第一步 GetIterator 在描述處理的流程。
假設有一個工作佇列。tasks 只負責 iterable 的入口;它交出的另一個物件才保存讀取進度,並負責 next() 與可選的 return()。這個例子故意不先把資料轉成另一個 Array,方便我們看見每次互動。
const tasks = {
[Symbol.iterator]() {
console.log("[iterable] 交出一個 iterator");
const items = ["整理需求", "撰寫內容", "發布文章"];
let index = 0;
return {
next() {
console.log("[iterator] 收到 next() 請求");
if (index === items.length) {
const result = { done: true };
console.log("[iterator] 交付", result);
return result;
}
const result = { value: items[index++], done: false };
console.log("[iterator] 交付", result);
return result;
},
return() {
console.log("[iterator] 收到 return():不再需要工作,準備收尾");
return { done: true };
},
};
},
};
for (const task of tasks) {
console.log("[for...of] 處理:", task);
if (task === "撰寫內容") {
break;
}
}
輸出會像這樣:
[iterable] 交出一個 iterator
[iterator] 收到 next() 請求
[iterator] 交付 { value: '整理需求', done: false }
[for...of] 處理: 整理需求
[iterator] 收到 next() 請求
[iterator] 交付 { value: '撰寫內容', done: false }
[for...of] 處理: 撰寫內容
[iterator] 收到 return():不再需要工作,準備收尾
把這次執行對回本文開頭的 TC39 地圖,會得到這三步:
| 範例中的動作 | 對應的規格概念 | 本次輸出 |
|---|---|---|
for...of 向 tasks 取得 iterator |
GetIterator | [iterable] 交出一個 iterator |
| 每輪索取下一筆,並依結果決定是否繼續 | next() 與 IteratorResult |
收到 next()、交付 { value, done } |
break 在尚未讀完時提早離開 |
IteratorClose | [iterator] 收到 return() |
這張表是把規格概念對回範例的閱讀地圖,不是規格演算法的逐字轉寫;接下來再拆開看每一步。
最容易忽略的地方是:for...of 把呼叫細節藏起來了。它開始執行時,會向 tasks 取得 iterator;每一輪再向該 iterator 要下一筆。這不是語言在解析程式碼時額外插入的呼叫,而是 for...of 在執行期遵循的行為。
for...of 不只負責向 iterator 要資料,也會觀察迴圈本體如何結束。
若本體正常跑完,它才呼叫下一次 next();若 iterator 回報 done: true,迴圈自然結束,不需要收尾通知。
反過來,只要在 iterator 尚未完成前以 break、函式內的 return 或例外離開,consumer 就表示不再需要後續資料;TC39 將 for...of 通知來源收尾的流程稱為 IteratorClose。
這個範例中,第二筆進入本體後執行 break,代表不再需要第三筆。
for...of 會先嘗試取得 iterator 的 .return();若它存在且可呼叫,就會呼叫它,之後才真正離開迴圈。輸出中的 return() 正是這個由流程控制觸發的隱藏步驟,不是範例手動呼叫。
break 只離開迴圈(或 switch);
return 則不是 for...of 的控制指令,而是由外層函式 body 允許、用來結束函式的語句。因此 for...of 位於函式內時可以寫 return,在頂層則不能寫;這不是 for...of 特別禁止,而是 Script 的文法不允許 return。
function findDraft(iterable) {
for (const task of iterable) {
if (task === "撰寫內容") {
return task;
}
}
}
三種提前離開的去向不同,收尾步驟則一致:
break
→ IteratorClose
→ 若存在,呼叫 iterator.return()
→ 繼續執行迴圈後的程式
函式內 return
→ IteratorClose
→ 若存在,呼叫 iterator.return()
→ 函式回傳值
throw
→ IteratorClose
→ 若存在,呼叫 iterator.return()
→ 例外繼續往外傳
不過在閱讀 TC39 規範時會發現 IteratorClose,再檢查 iterator資料來源有沒有提供 .return() 這個實作方法。
但其實在實務上,我們常用的控制流程通常寫在 for...of 本體裡面就很夠用了:
break
→ IteratorClose
→ 讀取 iterator.return
├─ 不存在 → 直接完成收尾並離開迴圈
├─ 是可呼叫的 function → 呼叫 return()
└─ 存在但不可呼叫 → 拋出 TypeError
for...of 想成一段資料消費過程for...of 可以看成一個向資料來源逐筆索取資料的 consumer(消費端)。
tasks 這類 iterable 則是資料來源的入口;它交出的 iterator 才負責保存進度,並在 consumer 每次提出請求時提供下一筆。
整個過程是:
for...of)先向 iterable 取得 iterator(規格中的 GetIterator)。iterator.next();iterator 回傳 IteratorResult { value, done }。done 不是 true 時,consumer 把 value 交給迴圈本體處理,接著再決定要不要索取下一筆。done: true 時,代表資料自然讀完;若 consumer 先用 break、函式內 return 或 throw 離開,則進入 IteratorClose,通知來源不再需要後續資料。因此 for...of 不會先把所有資料複製成 Array;它是「需要一筆,才拿一筆」。資料可以早已存在,也可以等到 next() 被呼叫時才產生。
主線已看見:for...of 需要 iterable,只有 length 和索引的物件不能直接用它。還有幾種常見語法,也會先向來源取得 iterator,再逐次讀取:
| 語法 | 如何使用 iterator |
|---|---|
[...iterable] |
讀取所有值,建立一個新的 Array。 |
fn(...iterable) |
讀取每一筆,依序作為函式引數。 |
const [first, second] = iterable |
讀取解構所需的值;若尚未讀完就停止,會嘗試關閉 iterator。 |
這些語法都不能直接接受只有 length 和索引的 array-like。
物件展開
{ ...object }則是另一條規則:它複製自身可列舉屬性,與 iterator 無關。
早期學JS展開運算子應該多少看過下面錯誤,終於知道原因爲何:
const arrayLike = { 0: "A", 1: "B", length: 2 };
// [...arrayLike]; // TypeError: arrayLike is not iterable
// console.log(...arrayLike); // TypeError: arrayLike is not iterable
const copiedObject = { ...arrayLike };
// { 0: "A", 1: "B", length: 2 }
Array.from(arrayLike); // ["A", "B"]
Array.from() 是這裡的特例:它的契約更寬。若來源有 iterable 入口,就依 iterator 逐筆讀取;若沒有 iterable 入口,才可改以 array-like 的 length 與索引建立 Array。
Array.from({ 0: "A", 1: "B", length: 2 });
// ["A", "B"]
不論走哪一條路徑,Array.from() 都會建立一個新的 Array;差別只在它怎麼取來源的值:
length 讀取 0 到 length - 1 的索引。[...iterable] 與 Array.from(iterable) 都會建立新的 Array,但都只是淺層處理:遇到物件值時,放進新 Array 的仍是同一個物件參考。fn(...iterable) 也會逐次讀取,但只是把每一筆交成函式引數,語意上不是建立一個可供使用的新 Array。
這不是 for...of 的例外規則,也不表示 array-like 自動變成 iterable;只是 Array.from() 額外支援另一條來源路徑。若物件明確提供了非 function 的 [Symbol.iterator],那是壞掉的 iterable 入口,Array.from() 會拋出錯誤,而不是悄悄退回 array-like 路徑。
for...of 作為 consumer,會先取得 iterator(GetIterator),再逐次呼叫 next();每個 IteratorResult { value, done } 決定是否把 value 交給迴圈本體。done: true 表示 iterator 自然完成;若在此之前用 break 或從函式 return 離開,for...of 會進入 IteratorClose,嘗試呼叫可選的 .return(),通知來源可自行收尾。Array.from() 除了 iterable,也有 array-like 的來源路徑。我們已經看見手寫 iterator 要自己保存位置、組出 { value, done },還可能實作收尾通知。
下一篇會改從程式碼的資料源頭角度出發:Generator 如何把「暫停在這裡、下一次再繼續」寫得像一般的程式?
.return() 的收尾處理for...of 的控制流程與 iterator closereturn 的函式範圍與 completionbreak 可離開的控制結構for...of
Array.from()