979 字
5 分鐘

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() 仍適合已取得的同步判定。把「等待條件」與「保留項目」拆開後,才能各自驗證順序、負載與錯誤政策。

參考資料:

MDN:Array.prototype.filter()

MDN:async function

MDN:Promise.all()

async filter 為什麼全部保留?JavaScript 非同步篩選的兩種寫法
https://laplusda.com/posts/javascript-async-filter-await/
作者
Zero
發佈於
2026-10-31
許可協議
CC BY-NC-SA 4.0