具代表性的面試主題

通用面試題:Iterator.concat 如何合併惰性資料來源?

通用困難
Offer.cc 編輯團隊發佈 更新

題幹

請實作一個合併陣列、Set 與產生器的惰性迭代器,並解釋 Iterator.concat 與陣列 concat、Iterator.from、flatMap 的邊界。

1. 題目與適用場景

日誌聚合服務要依優先級依序讀取記憶體陣列、去重後的 Set 與分頁產生器,消費者只需要前 100 筆記錄。請使用 Iterator.concat 合併這些 Iterable,保持惰性,說明無限資料來源、例外與提前停止時如何釋放底層迭代器。

2. 面試官考察點

  • 能否區分 Iterable 與 Iterator,並知道 Iterator.concat 要求參數實作 Iterable 協定。
  • 能否說明回傳值是新的惰性 Iterator,消費時才拉取各輸入,且輸入依序連接。
  • 能否辨識無限來源會遮蔽其後輸入,以及參數列表必須有限並受引擎參數數量限制。
  • 能否處理 return 關閉、TypeError、相容降級與不必要的陣列物化。

3. 回答前需要釐清的問題

  1. 每個輸入是可重複的 Iterable,還是已經推進過的單次 Iterator?
  2. 消費者需要串流介面,還是最終必須得到陣列?
  3. 任一輸入是否持有檔案、網路或資料庫游標,需要提前終止時關閉?
  4. 目標瀏覽器和 Node 版本是否原生提供 Iterator.concat,是否允許 polyfill?

4. 30 秒回答框架

我會先確認所有參數都實作 Symbol.iterator,再呼叫 Iterator.concat 回傳惰性 Iterator。它依輸入順序逐個取得迭代器,只有消費者呼叫 nexttake 或展開時才讀取資料;take(100) 後停止拉取並觸發底層關閉。無限輸入放在末尾,否則後續輸入永遠不可達;舊執行環境用產生器封裝相同的惰性與關閉語意。

5. 分步驟深入解答

第一步:確認 Iterable 邊界

Iterator.concat 接受 Iterable,而不只是擁有 next 的 Iterator。陣列、Set、產生器物件都可傳入;只有 Iterator 的物件需要先用 Iterator.from 包裝。這項限制讓 concat 可以取得並負責關閉目前輸入的迭代器。

第二步:建立惰性合併

js
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 更適合有限且已知的輸入集合。

追問四:如何驗證資源真的關閉?

寫一個實作 nextreturn 的測試 Iterable,記錄拉取次數和關閉次數;對 take、消費者中斷及回呼拋錯分別斷言停止拉取並執行 return

公開來源

同類題目