1. 題目與適用場景
日誌聚合服務要依優先級依序讀取記憶體陣列、去重後的 Set 與分頁產生器,消費者只需要前 100 筆記錄。請使用 Iterator.concat 合併這些 Iterable,保持惰性,說明無限資料來源、例外與提前停止時如何釋放底層迭代器。
2. 面試官考察點
- 能否區分 Iterable 與 Iterator,並知道
Iterator.concat要求參數實作 Iterable 協定。 - 能否說明回傳值是新的惰性 Iterator,消費時才拉取各輸入,且輸入依序連接。
- 能否辨識無限來源會遮蔽其後輸入,以及參數列表必須有限並受引擎參數數量限制。
- 能否處理
return關閉、TypeError、相容降級與不必要的陣列物化。
3. 回答前需要釐清的問題
- 每個輸入是可重複的 Iterable,還是已經推進過的單次 Iterator?
- 消費者需要串流介面,還是最終必須得到陣列?
- 任一輸入是否持有檔案、網路或資料庫游標,需要提前終止時關閉?
- 目標瀏覽器和 Node 版本是否原生提供
Iterator.concat,是否允許 polyfill?
4. 30 秒回答框架
我會先確認所有參數都實作 Symbol.iterator,再呼叫 Iterator.concat 回傳惰性 Iterator。它依輸入順序逐個取得迭代器,只有消費者呼叫 next、take 或展開時才讀取資料;take(100) 後停止拉取並觸發底層關閉。無限輸入放在末尾,否則後續輸入永遠不可達;舊執行環境用產生器封裝相同的惰性與關閉語意。
5. 分步驟深入解答
第一步:確認 Iterable 邊界
Iterator.concat 接受 Iterable,而不只是擁有 next 的 Iterator。陣列、Set、產生器物件都可傳入;只有 Iterator 的物件需要先用 Iterator.from 包裝。這項限制讓 concat 可以取得並負責關閉目前輸入的迭代器。
第二步:建立惰性合併
function* pages() {
yield { source: "page", id: 1 };
yield { source: "page", id: 2 };
}
const memory = [{ source: "memory", id: 1 }];
const unique = new Set([{ source: "set", id: 1 }, { source: "set", id: 2 }]);
const merged = Iterator.concat(memory, unique, pages());
const firstThree = merged.take(3).toArray();建立 merged 不會遍歷任何輸入;toArray 才開始消費。輸出順序是 memory、Set、pages,不同輸入中的值不會自動去重或轉換。
第三步:理解消費與單次狀態
回傳的 Iterator 保存目前輸入及其位置。一個消費者推進後,另一個消費者接手的是剩餘值;需要重播時應重新取得各 Iterable 的 Iterator。若只想查看前 N 筆,應在 concat 後使用 take,不要先展開成陣列。
第四步:處理無限輸入
任何輸入可以是無限 Iterable,結果也會無限。若第一個輸入無限,後續輸入永遠不會被讀取;因此有限批次應排在前面,或在消費端設定 take、逾時與取消邊界。不要寫 Iterator.concat(...infiniteIterables()),因為展開參數本身永遠無法完成。
第五步:關閉、例外與相容降級
當消費提前停止或拋出例外時,應讓目前迭代器的 return 有機會執行,釋放游標、檔案控制代碼或網路連線。自訂迭代器可在 finally 中記錄清理。缺少原生實作的執行環境可用產生器實作依序 yield*,並補齊 return 傳遞;降級不能偷偷把所有輸入複製到陣列。
6. 高品質示範回答
我會驗證參數是 Iterable,再用Iterator.concat建立惰性合併器,只有消費端呼叫take(100)或toArray時才讀取輸入。concat 依序取得各輸入迭代器,輸入中的值不會自動去重;無限來源若位於第一個位置會讓後續來源永遠不可達。提前停止與例外路徑要驗證底層return已關閉資源。對舊執行環境,我會用產生器保留惰性、順序、單次消費與關閉語意,避免預先物化陣列。
7. 常見錯誤
- 把只有
next的物件直接傳入 → 拋出 TypeError → 用Iterator.from包裝或實作Symbol.iterator。 - 誤以為建立 concat 會立即執行 → 產生隱藏 I/O → 強調消費操作才拉取資料。
- 把無限 Iterable 放在第一個 → 後續輸入永遠不可達 → 調整順序並設定
take或取消邊界。 - 先用展開運算符合併 → 記憶體和等待時間失控 → 只在明確需要陣列的邊界呼叫
toArray。 - 忽略提前終止的
return→ 游標洩漏 → 用帶計數的測試 Iterable 驗證關閉。
8. 追問及應對
追問一:它和陣列 concat 有什麼區別?
陣列 concat 立即處理已物化陣列並回傳陣列;Iterator.concat 接受 Iterable,回傳惰性 Iterator,可處理產生器和無限來源。
追問二:為什麼不能把所有 Iterator 都直接傳入?
只有 Iterator 卻不是 Iterable 時,concat 無法明確取得尚未存取輸入的所有權和關閉責任。先用 Iterator.from 使其符合 Iterable 邊界。
追問三:什麼時候用 flatMap?
當輸入本身是大量甚至無限的 Iterable 清單時,flatMap 可以逐項產生並展開,避免一次性展開參數清單;concat 更適合有限且已知的輸入集合。
追問四:如何驗證資源真的關閉?
寫一個實作 next、return 的測試 Iterable,記錄拉取次數和關閉次數;對 take、消費者中斷及回呼拋錯分別斷言停止拉取並執行 return。