794 字
4 分鐘

JavaScript flatMap 為什麼留下 undefined?跳過項目要回傳空陣列

本文目錄

用 flatMap() 轉換清單,遇到不要的資料就 return,結果清單多了 undefined。flatMap 只把 callback 回傳的陣列攤平一層;要跳過一筆應回傳 [],回傳 undefined 或 null 都會保留一個元素。

這個差別來自 ECMAScript flatMap 與 FlattenIntoArray 演算法:回傳值是陣列才進行攤平,其他值直接放進結果。下面的例子限定普通陣列,每個 JavaScript 區塊都可獨立存成 .mjs 後用 Node.js 執行。

[]、[undefined] 與沒寫 return,結果不同#

import assert from 'node:assert/strict';
assert.deepEqual([1, 2].flatMap(n => n === 1 ? [] : [n]), [2]);
assert.deepEqual([1, 2].flatMap(n => n === 1 ? undefined : [n]), [undefined, 2]);
assert.deepEqual([1].flatMap(() => {}), [undefined]);
assert.deepEqual([1].flatMap(() => [undefined]), [undefined]);
assert.deepEqual([1].flatMap(() => null), [null]);
assert.deepEqual([1].flatMap(() => [[1, 2]]), [[1, 2]]);
assert.deepEqual([1, , 2].flatMap(n => [n]), [1, 2]);
assert.deepEqual([1].flatMap(() => [, 'kept']), ['kept']);
console.log('flatMap return contracts: PASS');

回傳 [] 會貢獻零個元素;回傳 [undefined] 則貢獻一個明確的 undefined。來源空槽與 callback 回傳陣列內的空槽會被跳過,MDN flatMap也提供相同說明。若來源是 new Array(n),先確認它真的有索引值,可接著看 空槽與 Array.from 初始化。

外層只攤平一次。因此 [[1, 2]] 會變成一個 [1, 2] 元素,若資料契約是「每筆產生多個標籤」,callback 應直接回傳標籤陣列,避免額外包一層。

只排除缺值,保留 0、false 與空字串#

如果輸入是資料庫查詢結果,零與 false 可能都是有效值。先訂出排除條件,再決定回傳零個或一個元素:

import assert from 'node:assert/strict';
const input = [0, false, '', null, undefined, 'ready'];
const output = input.flatMap(value =>
value === null || value === undefined ? [] : [value]
);
assert.deepEqual(output, [0, false, '', 'ready']);
assert.deepEqual(input, [0, false, '', null, undefined, 'ready']);
const rows = [{ tags: ['js', 'css'] }, { tags: [] }];
assert.deepEqual(rows.flatMap(row => row.tags), ['js', 'css']);
console.log('explicit missing-value policy: PASS');

這個寫法表達的是「缺值就跳過,其餘保留」,不會把 falsy 全部移除。如果只做排除、沒有轉換需求,直接用條件明確的 filter() 也容易閱讀;需要一筆產生零筆、一筆或多筆時,再選 flatMap。

flatMap 回傳新陣列,但保留下來的物件仍共用原參照。上例使用基本型別;若接著修改物件屬性,資料隔離要依 structuredClone 的支援型別另外評估。

async callback 回傳的 Promise 會留在陣列裡#

flatMap 會同步呼叫 callback,Promise 屬於直接保留的非陣列值。最外層加 await 也只會拿到那個陣列,無法等候裡面的工作。下面用假的非同步查詢示範修正:

import assert from 'node:assert/strict';
async function loadTags(id) {
return id === 'skip' ? [] : [id, `${id}-extra`];
}
const ids = ['a', 'skip', 'b'];
const wrong = ids.flatMap(async id => loadTags(id));
assert.equal(wrong.length, 3);
assert.ok(wrong[0] instanceof Promise);
await Promise.all(wrong); // 等這批示範工作結束
const groups = await Promise.all(ids.map(id => loadTags(id)));
const tags = groups.flat();
assert.deepEqual(tags, ['a', 'a-extra', 'b', 'b-extra']);
console.log('await groups before flattening: PASS');

先以 map 建立 Promise 清單,等整批回傳陣列後,再 flat()。這個示範所有工作都成功;若部分工作失敗仍需保留結果,改依 Promise.allSettled 的結果分類處理。並行量也應依實際 API 限制安排。

三份範例由內容維護流程於 2026-11-05 在 Node.js v24.13.0 執行;驗證的是陣列與 Promise 行為,沒有執行外部 API 或框架畫面。

參考資料:

ECMAScript:Array.prototype.flatMap

MDN:Array.prototype.flatMap()

MDN:Promise.all()

JavaScript flatMap 為什麼留下 undefined?跳過項目要回傳空陣列
https://laplusda.com/posts/javascript-flatmap-undefined-skip-items/
作者
Zero
發佈於
2026-11-05
許可協議
CC BY-NC-SA 4.0