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 或框架畫面。
參考資料: