1142 字
6 分鐘

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 的任務清單驗證,不要把初始化與聚合規則混在一起。

參考資料:

MDN:Array.prototype.map()

MDN:Array.from()

MDN:Array.prototype.fill()

ECMAScript:Array.prototype.map

MDN:delete 與陣列元素

new Array(n).map() 為什麼沒執行?空槽與 Array.from 的初始化差別
https://laplusda.com/posts/javascript-new-array-map-empty-slots/
作者
Zero
發佈於
2026-10-27
許可協議
CC BY-NC-SA 4.0