1. 題目與適用場景
你需要處理一個可能包含數百萬筆記錄的同步迭代器:過濾無效記錄,映射成展示物件,取得前 100 筆並計算總金額。團隊原先先用展開運算子轉成陣列,再呼叫 map、filter 和 reduce,記憶體峰值過高。請使用 JavaScript Iterator Helpers 改寫,說明惰性求值、迭代器協定、提前終止、例外清理和舊執行環境降級。
2. 面試官考察點
- 是否理解 Iterator 與 Iterable 的差別,以及 Iterator Helpers 回傳的仍是可繼續消費的迭代器。
- 是否能用 map、filter、take、find、reduce 和 toArray 組合惰性流水線。
- 是否知道迭代器有狀態且通常只能前進一次,提前終止會觸發底層 return 清理。
- 是否處理無限或昂貴資料源、例外、相容性和不應隨意物化陣列的邊界。
3. 回答前需要釐清的問題
- 資料源是同步 Iterator、Iterable,還是非同步分頁 API?
- 結果是否必須一次回傳陣列,還是可以繼續串流消費?
- 取得前 N 筆後是否需要關閉網路、檔案或資料庫游標?
- 目標 Node 和瀏覽器版本是否原生支援 Iterator Helpers,是否允許 polyfill?
4. 30 秒回答框架
我會先把 Iterable 正規化為 Iterator,再鏈式呼叫 filter、map 和 take,只有在邊界需要陣列時才呼叫 toArray。map 和 filter 不會立刻遍歷來源資料,reduce 和 toArray 才會開始消費;take 到數量後應停止向底層請求,並透過迭代器關閉協定釋放資源。迭代器有狀態,不能在多個消費者之間任意重用。舊執行環境用相容實作或手寫生成器保持相同語意,並測試例外、提前終止和大型資料記憶體占用。
5. 分步驟深入解答
第一步:區分 Iterator 和 Iterable
Iterable 提供 Symbol.iterator 方法,每次可以產生一個 Iterator;Iterator 提供 next 方法並回傳 done 和 value。Iterator.from 可以把符合迭代協定的輸入正規化。Iterator Helpers 作用在迭代器上,產生惰性 helper;它們不會把整個來源資料自動複製到陣列。
第二步:建立惰性 map、filter、take 管道
下面的函式只在消費結果時讀取來源迭代器。filter 先判斷記錄,map 再轉換,take 在達到數量後停止,避免無關記錄繼續流入。
function topAmounts(source, limit) {
return Iterator.from(source)
.filter((row) => row.status === "paid")
.map((row) => ({ id: row.id, amount: row.cents / 100 }))
.take(limit);
}
const firstHundred = topAmounts(records(), 100).toArray();第三步:理解消費時機和單次遍歷
建立 helper 不會呼叫回調;呼叫 next、forEach、find、reduce 或 toArray 時才會拉取來源值。一個 iterator 儲存目前位置,第一次消費會改變狀態,第二次消費可能已經完成。需要多個獨立結果時,為每個結果建立新的來源 iterator,不要共享同一個已消費實例。
第四步:提前終止、return 和例外
take、find 等操作可以在找到結果後提前結束。若底層 iterator 提供 return,helper 在結束或例外時應讓資源有機會關閉,例如檔案控制代碼或分頁游標。業務程式碼仍應在 finally 中釋放自己擁有的資源,並驗證來源 iterator 在回調丟錯和消費者主動停止時都能清理。
第五步:選擇物化邊界和相容降級
toArray 會把剩餘結果物化,應只放在確實需要隨機存取、序列化或 UI 批次渲染的邊界。無限 iterator、巨大分頁和昂貴計算應繼續保持惰性。若目標執行環境沒有原生 Iterator Helpers,可用受控 polyfill 或生成器封裝 map、filter、take;降級實作要保留單次消費、提前終止和例外傳播語意,不要把所有資料偷偷轉成陣列。
6. 高品質示範回答
我會用 Iterator.from 正規化輸入,再鏈式呼叫 filter、map 和 take,只有輸出邊界需要陣列時才呼叫 toArray。helper 的回調在消費時執行,所以大型資料源不會提前展開;take 或 find 找到結果後應停止拉取,並透過 return 讓底層游標釋放。iterator 有狀態且通常只能消費一次,多份結果要重新建立來源 iterator。對舊 Node 或瀏覽器,我會用 polyfill 或生成器實作同樣的惰性和關閉語意,並測試例外、提前停止、資源釋放和記憶體峰值。
7. 常見錯誤
- 一開始就寫展開運算子 → 整個資料源被物化 → 把 toArray 留在真正需要陣列的邊界。
- 誤以為建立 map 會立即執行回調 → 偵錯時看不到副作用 → 說明消費操作才會觸發拉取。
- 重複消費同一個 iterator → 第二次結果為空或不完整 → 為每個消費者建立新的來源。
- take 後仍繼續請求分頁 → 浪費網路和資源 → 驗證提前終止會呼叫底層 return。
- polyfill 只複製陣列結果 → 無限來源和例外語意改變 → 保持惰性、單次消費和例外傳播。
8. 追問及應對
追問一:Iterator Helpers 和陣列方法的核心差異是什麼?
陣列方法面對已物化陣列,通常會立即遍歷並產生陣列結果;Iterator Helpers 以迭代器為輸入,map、filter、take 等轉換是惰性的,可以只拉取需要的元素。
追問二:什麼時候仍然應該呼叫 toArray?
當邊界需要隨機索引、序列化、批次傳給只接受陣列的 API 或一次渲染少量結果時呼叫。對於巨大或無限來源,應避免物化並繼續串流消費。
追問三:iterator 為什麼不能任意重用?
它儲存目前游標,next 會改變內部狀態。第一個消費者讀取後,第二個消費者接手的是剩餘位置;要重播必須重新取得 Iterable 產生的新 iterator。
追問四:如何驗證提前終止真的釋放資源?
用帶計數和 return 記錄的測試 iterator,呼叫 take 或 find 後斷言拉取次數停止且 return 被呼叫;再覆蓋回調丟錯和消費者中斷路徑。