async filter 為什麼全部保留?JavaScript 非同步篩選的兩種寫法
items.filter(async item => await isAllowed(item)) 把每筆資料都留下,即使條件最後回傳 false。原因是 filter 當下收到 Promise 物件,把它當成真值;它不會等待 Promise 裡面的布林結果。 在整個 filter() 外面加 await 也沒有用。
先選擇循序等待,或先並行取得判定再用同步 filter()。兩種方法的差別是何時啟動工作,不只是寫法長短。
await 放在外層,仍然等不到判定
filter 的 callback 契約以回傳值的 truthiness 決定是否保留;async function則回傳 Promise。這段可獨立存成 .mjs 執行:
import assert from 'node:assert/strict';
const input = [1, 2, 3];const wrong = await input.filter(async () => false);assert.deepEqual(wrong, [1, 2, 3]);assert.equal(Boolean(Promise.resolve(false)), true);console.log('已重現:async false 仍全部保留');這裡不是網路慢到結果沒更新,而是篩選早已完成。若 callback 拒絕,原本的同步 filter() 也不會替你收集錯誤,所以不要拿這種寫法呼叫真實 API。
需要一筆完成才做下一筆,用 for…of
下面兩個 helper 都限定一般、沒有空槽的陣列,predicate 回傳布林或 Promise 布林。執行期間不要修改輸入或 predicate 依賴的共享狀態。
async function filterSerial(items, predicate) { const snapshot = items.slice(); const kept = []; for (const [index, item] of snapshot.entries()) { if (await predicate(item, index)) kept.push(item); } return kept;}
async function filterParallel(items, predicate) { const snapshot = items.slice(); const decisions = await Promise.all( snapshot.map((item, index) => predicate(item, index)), ); return snapshot.filter((_, index) => decisions[index]);}把這組與下一組驗收放在同一份 .mjs 執行。filterSerial 適用於有順序依賴、需要前一筆完成再開始下一筆的工作;遇到拒絕就停止,後面的 predicate 不再啟動。它保留原物件參照,不做深拷貝。
如果每筆互不相依,資料量也受控,可以用 filterParallel。Promise.all() 的結果依輸入順序排列,所以晚完成的第一筆仍對應第一個判定,不會因完成順序不同而選錯項目。
驗收不能只看最後剩幾筆
下面檢查保留順序、空陣列,以及 predicate 拒絕時的行為。以手動 gate 讓第一筆晚於後兩筆完成,不依賴毫秒數猜測排程:
import assert from 'node:assert/strict';
for (const filter of [filterSerial, filterParallel]) { assert.deepEqual(await filter([1, 2, 3], async n => n !== 2), [1, 3]); assert.deepEqual(await filter([], async () => true), []); await assert.rejects( filter([1, 2, 3], async n => { if (n === 2) throw new Error('判定服務失敗'); return true; }), /判定服務失敗/, );}
let releaseFirst;const gate = new Promise(resolve => { releaseFirst = resolve; });const completed = [];const pending = filterParallel([1, 2, 3], async n => { if (n === 1) await gate; completed.push(n); return n !== 2;});assert.deepEqual(completed, [2, 3]);releaseFirst();assert.deepEqual(await pending, [1, 3]);assert.deepEqual(completed, [2, 3, 1]);console.log('篩選、空陣列、拒絕與完成順序檢查通過');本文在 Node.js 執行以上範例,沒有測試外部 API、瀏覽器畫面或服務限流。若工作量很大,這個 map() 會立即啟動整批工作;它不是並行數控制器。predicate 若是普通函式且同步拋錯,map() 也可能中途停止,不能宣稱每筆都必定啟動。
查不到與查詢失敗,先決定要不要分開
某筆權限查詢失敗時,直接 catch(() => false) 會把故障轉成「不符合條件」。如果畫面因此少了一筆訂單,讀者無法知道是被篩掉還是服務故障。上面兩個 helper 採整體拒絕,讓呼叫端決定如何顯示錯誤。
需要保留成功項目並顯示失敗原因時,應另建包含 status、原始資料與錯誤的結果契約,可參考 Promise.allSettled 的部分失敗處理。而且 Promise.all() 拒絕不會取消已經啟動的工作;若後續要清理共享資源,還要檢查 等待剩餘工作結束的生命週期。
原生 filter() 仍適合已取得的同步判定。把「等待條件」與「保留項目」拆開後,才能各自驗證順序、負載與錯誤政策。
參考資料: