new Array(n).map() 為什麼沒執行?空槽與 Array.from 的初始化差別
想產生三筆表單資料,寫了 new Array(3).map(() => ({ value: '' })),結果 callback 一次都沒執行。new Array(3) 建立的是長度為 3、但沒有那些索引屬性的陣列;map 會跳過空槽。 length 並不是已填入元素的數量。
若目標是「每個位置建立一份新資料」,直接用 Array.from({ length: 3 }, (_, index) => ...)。不要為了讓 map 能跑就隨手 fill({}),那會把同一個物件放進每個位置。
讀起來都是 undefined,卻不代表同一種陣列
下面可獨立存成 holes-check.mjs,執行 node holes-check.mjs。範例使用未修改原型的普通陣列:
import assert from 'node:assert/strict';
const holes = new Array(3);const explicit = [undefined, undefined, undefined];let holesVisited = 0;let explicitVisited = 0;
const mapped = holes.map(() => { holesVisited += 1; return 'item'; });explicit.map(() => { explicitVisited += 1; return 'item'; });
assert.equal(holes.length, 3);assert.equal(holes[0], undefined);assert.equal(explicit[0], undefined);assert.equal(Object.hasOwn(holes, 0), false);assert.equal(Object.hasOwn(explicit, 0), true);assert.equal(holesVisited, 0);assert.equal(explicitVisited, 3);assert.equal(mapped.length, 3);assert.equal(Object.hasOwn(mapped, 0), false);console.log('length、空槽與明確 undefined 的差別檢查通過');MDN map 文件說明,稀疏陣列經過 map 後仍保留空槽。上例的回傳陣列長度也是 3,因此只檢查 mapped.length === 3,會漏掉「根本沒建立資料」的問題。
Object.hasOwn 能用來檢查該索引是否由陣列自身持有。嚴格來說,ECMAScript map 演算法使用的是 HasProperty,會考慮原型鏈;本文沒有處理原型上額外放入數字索引或 Proxy 的特殊情況。不要把這個普通陣列檢查推論成所有物件的規則。
用 Array.from 在每個位置呼叫工廠函式
以下是另一份可獨立執行的 .mjs:
import assert from 'node:assert/strict';
const rows = Array.from({ length: 3 }, (_, index) => ({ id: index + 1, value: '', errors: [],}));
assert.deepEqual(rows.map(row => row.id), [1, 2, 3]);assert.equal(Object.hasOwn(rows, 0), true);assert.notEqual(rows[0], rows[1]);assert.notEqual(rows[0].errors, rows[1].errors);rows[0].value = '只改第一列';rows[0].errors.push('必填');assert.equal(rows[1].value, '');assert.deepEqual(rows[1].errors, []);
let calls = 0;assert.deepEqual(Array.from({ length: 0 }, () => { calls += 1; return {}; }), []);assert.equal(calls, 0);console.log('每列物件與巢狀陣列互相獨立');Array.from 文件說明它不會建立稀疏陣列;缺少的索引值以 undefined 處理,再交給 mapper。上例把物件與 errors: [] 都放在 callback 裡建立,每次呼叫才會得到新的參照。
這裡的 id 只是本機列序號,沒有宣稱它是跨分頁或資料庫唯一鍵。若列數來自 API 或使用者輸入,也要先驗證合理上限;範例固定使用 3,沒有提供大量配置的記憶體保護。
fill({}) 讓每列都有值,也讓每列共用同一份值
下面的反例與修正可以一起執行:
import assert from 'node:assert/strict';
const shared = new Array(3).fill({ errors: [] });shared[0].errors.push('第一列錯誤');assert.equal(shared[0], shared[1]);assert.deepEqual(shared[2].errors, ['第一列錯誤']);
const independent = new Array(3).fill(undefined).map(() => ({ errors: [] }));independent[0].errors.push('只留在第一列');assert.notEqual(independent[0], independent[1]);assert.deepEqual(independent[1].errors, []);console.log('fill 共用參照與逐項建立檢查通過');MDN fill 文件指出,填入物件時,每個位置參照的是同一個物件。fill(undefined).map(factory) 可以運作,是因為先建立了實際索引,再逐項呼叫工廠;若一開始就是要初始化新資料,Array.from 把意圖寫在同一步,較容易審查。
同樣地,若 mapper 每次都回傳外面同一個 template,即使用 Array.from 也會共用它。重點在於每次是否建立需要獨立的層級;淺層展開物件仍可能保留共用的巢狀陣列。需要複製既有資料時,再依資料型別檢查 structuredClone 的限制。
delete 會留下空槽,要移除一列應選不同操作
下面是第四份獨立範例:
import assert from 'node:assert/strict';
const deleted = ['a', 'b', 'c'];delete deleted[1];assert.equal(deleted.length, 3);assert.equal(Object.hasOwn(deleted, 1), false);assert.deepEqual(deleted.map(value => value.toUpperCase()), ['A', , 'C']);
const removed = ['a', 'b', 'c'];removed.splice(1, 1);assert.deepEqual(removed, ['a', 'c']);assert.equal(removed.length, 2);console.log('delete 留洞,splice 才移動後續元素');依 MDN delete 說明,刪除陣列元素不會改變 length。要從表單列清單移除一項並讓後續位置前移,可用 splice;若狀態管理要求回傳新陣列,則另依產品的識別鍵使用 filter 或支援環境中的 toSpliced。不要用 delete 後再把 map 跳過的項目誤認為已縮短清單。
四份刊登範例由內容維護流程於 2026-10-27 在 Node.js v24.13.0 執行,涵蓋空槽、undefined、獨立巢狀資料、零長度、共用參照與刪除行為。未測 DOM、框架渲染或瀏覽器效能;這些是陣列資料行為的驗證。若問題出在多個非同步任務結果對應,接著看 allSettled 的任務清單驗證,不要把初始化與聚合規則混在一起。
參考資料: