具代表性的面試主題

程式設計面試題:如何用 JavaScript Iterator Helpers 建立惰性資料管道?

程式題中等
Offer.cc 編輯團隊發佈 更新

題幹

請實作一個從分頁資料源中篩選、轉換並取得前 N 筆結果的惰性管道,並解釋 Iterator Helpers 與陣列方法、單次消費和提前終止的差異。

1. 題目與適用場景

你需要處理一個可能包含數百萬筆記錄的同步迭代器:過濾無效記錄,映射成展示物件,取得前 100 筆並計算總金額。團隊原先先用展開運算子轉成陣列,再呼叫 map、filter 和 reduce,記憶體峰值過高。請使用 JavaScript Iterator Helpers 改寫,說明惰性求值、迭代器協定、提前終止、例外清理和舊執行環境降級。

2. 面試官考察點

  • 是否理解 Iterator 與 Iterable 的差別,以及 Iterator Helpers 回傳的仍是可繼續消費的迭代器。
  • 是否能用 map、filter、take、find、reduce 和 toArray 組合惰性流水線。
  • 是否知道迭代器有狀態且通常只能前進一次,提前終止會觸發底層 return 清理。
  • 是否處理無限或昂貴資料源、例外、相容性和不應隨意物化陣列的邊界。

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

  1. 資料源是同步 Iterator、Iterable,還是非同步分頁 API?
  2. 結果是否必須一次回傳陣列,還是可以繼續串流消費?
  3. 取得前 N 筆後是否需要關閉網路、檔案或資料庫游標?
  4. 目標 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 在達到數量後停止,避免無關記錄繼續流入。

js
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 被呼叫;再覆蓋回調丟錯和消費者中斷路徑。

公開來源

同類題目

相關面試工具

用 Screenshot 處理演算法題

截圖題目後,依序看約束、解法、程式碼、邊界條件和複雜度。

查看工具