我想快速造一个长度为 5、每项都初始化好的数组,顺手写了 new Array(5).map,结果 map 里的函数一次都没执行、拿到的还是一个全是空的数组,排查半天才发现 new Array(5) 造出来的根本不是 5 个 undefined、而是 5 个会被 map 跳过的空位的深度复盘
这是一次让我对"'没有值'和'有一个空值'是两码事"有了刻骨认知的事故。我想快速生成一个长度为 5、每一项都按规则初始化好的数组(比如 [0,1,2,3,4]、或一组初始对象)。我很自然地写了 new Array(5).map((_, i) => i)——先用 new Array(5) 造一个长度 5 的数组,再用 map 给每一项赋值。在我脑子里,这就是"造 5 个格子、再逐个填上值",顺理成章。
可结果让我懵了:map 里的函数一次都没执行(我加了 console.log 验证,一行都没打印),最后拿到的还是一个长度为 5、却什么都没填进去的"空"数组。我一开始以为是 map 用错了、是箭头函数写错了,反复检查语法都没问题。直到我把 new Array(5) 的结果打印出来仔细看,才恍然大悟:new Array(5) 造出来的,不是 5 个值为 undefined 的元素,而是 5 个"空位(hole / empty slot)"——这个数组的 length 是 5,但它实际上一个元素都没有(是个"稀疏数组")。而 map、forEach、filter 这些数组方法,会跳过空位——它们只对"真正存在的元素"执行回调。于是我的 map 面对 5 个空位,自然一次回调都没跑、什么也没填。
故障现场:new Array(5) 是 5 个空位,map 全跳过
我把这个"map 不执行"的现象还原出来,问题一目了然:
// 我想: 造长度 5 的数组, 再用 map 填值
const arr = new Array(5).map((_, i) => i);
console.log(arr); // [ <5 empty items> ] ✗ map 没执行, 还是空位!
// ↑ 期望 [0,1,2,3,4], 实际是 5 个空位, map 的回调一次没跑
// 真相: new Array(5) 造的是 5 个"空位", 不是 5 个 undefined
const holes = new Array(5);
console.log(holes); // [ <5 empty items> ] ← 空位, 不是 undefined
console.log(holes.length); // 5 ← 长度是 5
console.log(0 in holes); // false ← 但索引 0 处【没有元素】!(稀疏)
console.log(holes[0]); // undefined ← 取值给 undefined, 但它不是"存在的 undefined"
// map/forEach/filter 会【跳过空位】, 只对真正存在的元素执行
holes.map(() => { console.log("跑了一次"); }); // 一次都不打印! 全跳过
// 对比: 真正存在的 undefined 元素, map 不会跳
const reals = [undefined, undefined, undefined];
reals.map(() => console.log("跑了")); // 打印 3 次 ✓ (元素真实存在)
// 正解: 用 Array.from 或 fill, 造出"真正存在的元素", 再 map
const a1 = Array.from({ length: 5 }, (_, i) => i); // [0,1,2,3,4] ✓
const a2 = Array.from(new Array(5), (_, i) => i); // ✓ from 会把空位当 undefined
const a3 = new Array(5).fill(0).map((_, i) => i); // fill 先填实再 map ✓
const a4 = [...new Array(5)].map((_, i) => i); // 展开会把空位变 undefined ✓
看着"length 是 5、但 0 in arr 是 false、map 全跳过",我才彻底明白:一个数组的"长度(length)"和"里面到底有没有那个元素"是两件不同的事。new Array(5) 只是把 length 设成了 5,却没有真正创建任何元素——那 5 个位置是"空位",它们"不存在"(0 in arr 为 false),只是当你取值时 JS 返回 undefined 给你。而 map/forEach/filter 是按"元素是否真实存在"来决定跑不跑回调的,空位它们直接跳过。我以为 new Array(5) 给了我"5 个待填的 undefined",其实它给的是"5 个根本不存在的空位";map 对着不存在的东西,当然无从执行。我以为我有 5 个空格子等着填,其实那是 5 个连格子都还没真正建起来的"窟窿"。
第一件事:搞懂稀疏数组与空位——"长度"不等于"元素真实存在"
冷静下来,我去把"JS 的稀疏数组与空位"这一课认真补了,才明白这个"map 不执行"的根源:
【空位(hole) vs undefined 元素, 以及 map 为何跳过空位】
JS 数组可以是"稀疏(sparse)"的: length 大于实际存在的元素个数
产生空位的常见方式:
- new Array(5) ← 只设了 length=5, 没创建任何元素 → 5 个空位
- [1, , 3] ← 中间是空位
- arr[10] = "x" ← arr.length 变 11, 但 1~9 是空位
- delete arr[2] ← 删掉元素留下空位(length 不变)
"空位"和"值为 undefined 的元素"是两回事:
- 空位: 该索引处【没有这个属性】, key 'i' in arr 为 false
- undefined 元素: 该索引处【有这个属性, 值是 undefined】, 'i' in arr 为 true
- 两者用 arr[i] 取值都得 undefined, 但"存不存在"不同 → 行为不同
哪些方法跳过空位(只对真实存在的元素执行):
- map / forEach / filter / reduce / some / every ... 都跳过空位
- 所以 new Array(5).map(...) 的回调一次不跑
哪些方法把空位当 undefined 处理(不跳过):
- Array.from、扩展运算符 [...arr]、for...of、join、fill、keys/values
- 所以这些可以"填实"空位
正解: 先把空位变成"真实存在的元素", 再 map
- Array.from({length:5}, (_, i) => i) ← 最常用, 一步到位
- new Array(5).fill(undefined).map(...) ← fill 先填实
- [...new Array(5)].map(...) ← 展开填实
核心: 数组的 length 只是个数字, 不代表那些位置都有真实元素;
要操作"每一项", 先确保每一项真实存在
这一下点醒了我:我把"数组的 length 是 5"等同于了"这个数组里有 5 个真实存在的元素(哪怕值是 undefined)",可这俩是两回事——new Array(5) 只设了 length、没创建元素,那 5 个位置是"不存在"的空位。而 map 这类方法是按"元素是否真实存在"决定跑不跑回调的,所以对着一堆空位,它一次都不跑。"空位(根本没有这个元素)"和"值为 undefined 的元素(有这个元素、只是值空)"是两种不同的"空",取值时都返回 undefined 把我骗过去了,可它们在 map 等方法面前行为截然不同。不是 map 失灵,是我喂给它的根本不是"元素",而是"元素的窟窿"。
第二件事:正解——用 Array.from / fill 造出真实元素,再 map
找到根因,正解就清晰了:要"造一个长度 N、每项初始化好的数组",别用 new Array(N).map(map 跳过空位);改用 Array.from({length:N}, (_, i) => ...)(一步到位、最常用)、或 new Array(N).fill(v).map(...)(fill 先把空位填实再 map)、或 [...new Array(N)].map(...)(展开把空位变成真实的 undefined)。核心是先让每一项真实存在,再去 map。
// 错误: new Array(N).map —— map 跳过空位, 回调不执行
const bad = new Array(5).map((_, i) => i); // ✗ [ <5 empty> ]
// 正解1: Array.from + 映射函数(最常用, 一步生成)
const a = Array.from({ length: 5 }, (_, i) => i); // [0,1,2,3,4] ✓
const objs = Array.from({ length: 3 }, () => ({ done: false })); // 3 个独立对象 ✓
// 正解2: fill 先填实, 再 map
const b = new Array(5).fill(0).map((_, i) => i * 2); // [0,2,4,6,8] ✓
// 注意 fill(对象) 是同一个对象引用! 要独立对象用 Array.from 或 map 新建
const shared = new Array(3).fill({}); // ✗ 3 个元素是同一个对象
const indep = Array.from({ length: 3 }, () => ({})); // ✓ 3 个独立对象
// 正解3: 展开把空位变 undefined, 再 map
const c = [...new Array(5)].map((_, i) => i); // ✓
// 简单填同一个值, 直接 fill 就够(不需要 map)
const zeros = new Array(5).fill(0); // [0,0,0,0,0] ✓
// 取值时也别被空位骗: 判断"是否真有这个元素"用 in, 别只看 arr[i]!==undefined
if (i in arr) { /* 这个位置真有元素 */ }
这套做法的精髓,是在用 map 等"只处理真实元素"的方法之前,先确保数组里的每一项都是真实存在的元素、而非空位:Array.from({length:N}, fn) 直接按长度生成真实元素并映射,是最干净的写法;fill 先把空位填上实值;展开运算符把空位转成真实的 undefined。顺带要警惕 fill(对象) 会让所有元素共享同一个对象引用(那是另一个坑),要独立对象就用 Array.from(..., () => ({})) 每次新建。不是 map 不能用,而是别拿它去 map 一堆不存在的空位。
【造/操作数组, 几条原则】
1. new Array(N) 造的是 N 个空位(稀疏数组), 不是 N 个 undefined
2. map/forEach/filter/reduce 跳过空位 → new Array(N).map 回调不执行
3. 造"长度 N + 每项初始化": 用 Array.from({length:N}, (_, i)=>...)(首选)
4. 或 new Array(N).fill(v) 先填实再 map; 或 [...new Array(N)] 展开再 map
5. fill(对象) 所有元素共享同一引用; 要独立对象用 Array.from(..., ()=>({}))
6. 判断某位置是否真有元素用 i in arr, 别只靠 arr[i] !== undefined
第三件事:其他"看着有、其实是空/不存在"的同类坑
顺着"长度/外观显示有、实际元素不存在"这条线,我把同类的坑都梳理了一遍,它们都源于"把'表面有'当成了'真实存在'":
第一个,delete 数组元素留下空位。delete arr[1] 不会让数组变短,只是把该位置变成空位,length 不变、后续 map 跳过它。要删元素用 splice。
第二个,对象属性"存在但值为 undefined" vs "不存在"。obj.x === undefined 区分不出这两种,要用 "x" in obj 或 hasOwnProperty 判断属性是否真的存在。
第三个,Map/Set 的 has vs get。map.get(k) 返回 undefined 可能是"没这个键"也可能是"键的值就是 undefined",要用 map.has(k) 判断键是否存在。
第四个,JSON 里字段缺失 vs 值为 null。字段不存在和字段值为 null 是两回事,处理时要分清(和稀疏数组同源的"缺失 vs 存在但为空")。
第四件事:空位 vs undefined 元素,一张表对照
我把"空位(hole)"和"值为 undefined 的元素"的差别整理成一张表,这是我现在判断数组能不能正常 map 的依据:
| 维度 | 空位 hole(new Array(N)/[,,]/delete) | undefined 元素([undefined]) |
|---|---|---|
| 该位置存在吗 | 不存在(i in arr 为 false) | 存在(i in arr 为 true) |
| arr[i] 取值 | undefined | undefined |
| map/forEach | 跳过, 回调不执行 | 执行回调 |
| Array.from/展开 | 会被当成 undefined(填实) | 保持 undefined |
| typeof 看不出 | 都 undefined, 用 in 才分得清 | — |
| 怎么造出真实元素 | Array.from/fill/展开 | 本就是真实元素 |
这张表让我看清:空位和 undefined 元素用 arr[i] 取值都得 undefined、把人骗过去,可"该位置到底存不存在"不同——空位不存在,map 会跳过它。判断要用 i in arr 而非 arr[i] !== undefined。要让 map 正常处理每一项,先用 Array.from 等把空位填成真实元素。
第五件事:我对"new Array(n) 造数组"的几个想当然
这次事故,本质是我把"length 为 N"当成了"有 N 个真实元素"。把这些想当然列出来,每一条都值得警惕:
| 我曾经的想当然 | 事故教我的真相 |
|---|---|
| "new Array(5) 就是 5 个 undefined" | 是 5 个空位(不存在的元素), 不是 undefined |
| "new Array(5).map 能给每项赋值" | map 跳过空位, 回调一次不执行 |
| "length 是 5 就有 5 个元素" | length 只是数字, 稀疏数组里位置可能不存在 |
| "arr[i] 是 undefined 就是没值/空位" | 分不清空位和 undefined 元素, 要用 i in arr |
| "map 没执行是我写错了" | 常是数组里全是空位、被 map 跳过了 |
| "new Array(3).fill({}) 是 3 个对象" | 是同一个对象的 3 个引用; 要独立用 Array.from |
第六件事:造数组、遍历数组时,我现在的自检习惯
现在每当我造一个数组要逐项初始化、或排查"map/forEach 没执行",我都会先按这张图问自己:
这张图的精髓,是"造数组逐项初始化别用 new Array(N).map(空位被跳过),用 Array.from({length:N}, fn) 直接造真实元素"。写时就逐项初始化用 Array.from、填同值用 fill、要独立对象用 Array.from 新建、排查就看 map 没执行是不是数组里全是空位。这套习惯,让我从"length 是 N 就有 N 个元素"变成了"先确保每一项真实存在再去操作"——核心始终是:JS 数组可以是稀疏的(length 大于实际存在元素数),new Array(N) 只设了 length=N 却没创建任何元素、那 N 个位置是空位(hole);空位和值为 undefined 的元素是两回事——空位该位置不存在(i in arr 为 false)、undefined 元素该位置存在(in 为 true),两者 arr[i] 取值都得 undefined 但行为不同;map/forEach/filter/reduce 会跳过空位只对真实存在的元素执行回调,所以 new Array(N).map 的回调一次不跑;正解是先把空位变成真实元素再 map——Array.from({length:N}, (_, i)=>...) 一步到位、new Array(N).fill(v).map(...)、[...new Array(N)].map(...),判断位置是否真有元素用 i in arr 而非 arr[i]!==undefined。
我立下的几条规矩
这场"new Array(5).map 不执行"的事故,换来了我写 JavaScript 时,刻进骨子里的几条铁律:
- new Array(N) 造的是 N 个空位(稀疏数组),不是 N 个 undefined;length 是 N 不代表有 N 个真实元素。
- 空位(hole)和值为 undefined 的元素是两回事:空位"不存在"(i in arr 为 false),undefined 元素"存在但值空"。
- map/forEach/filter/reduce 跳过空位、只处理真实存在的元素,所以 new Array(N).map 回调一次不跑。
- 造"长度 N 且每项初始化"用 Array.from({length:N}, (_, i)=>...)(首选),或 fill 后再 map、或展开后再 map。
- new Array(N).fill(对象) 所有元素共享同一引用;要独立对象用 Array.from(..., ()=>({}))。
- 判断某位置是否真有元素用 i in arr,别只靠 arr[i] !== undefined(分不清空位)。
- delete arr[i] 留下空位、length 不变;真要删元素用 splice。
附:一段把"空位被跳过、Array.from 填实"摆清楚的小实验
这是我后来写的一段小实验,把空位和真实元素在各种方法下的行为并排跑出来——它把"声称的规模 vs 真实的内容"变成了眼见为实的对比,现在我也常拿它给同事讲清这个坑:
const holes = new Array(3); // 3 个空位
const reals = [undefined, undefined, undefined]; // 3 个真实的 undefined 元素
console.log("holes.length:", holes.length, " reals.length:", reals.length); // 3 3
console.log("0 in holes:", 0 in holes); // false ← 空位: 位置不存在
console.log("0 in reals:", 0 in reals); // true ← 真实元素: 位置存在
let cntH = 0, cntR = 0;
holes.forEach(() => cntH++); // map/forEach 跳过空位
reals.forEach(() => cntR++);
console.log("holes 跑了", cntH, "次"); // 0 ← 空位全跳过, 一次没跑!
console.log("reals 跑了", cntR, "次"); // 3 ← 真实元素正常跑
// 用 Array.from 把"声称的规模"填实成"真实的内容", 再 map 就正常了
const filled = Array.from(holes); // from 把空位当 undefined → 真实元素
console.log("0 in filled:", 0 in filled); // true ← 填实了
console.log(Array.from({length:3}, (_,i)=>i)); // [0,1,2] ← 一步造真实元素
// 关键结论:
// holes 和 reals 的 length 都是 3(声称的规模一样),
// 但 forEach 一个跑 0 次、一个跑 3 次(真实内容不同)
// 分辨它俩, 看 in(位置是否真存在), 别看 length 也别看 arr[i]
这段实验把这次的教训摆得明明白白:holes 和 reals 的 length 都是 3(声称的规模一样),可 forEach 一个跑了 0 次、一个跑了 3 次(真实内容天差地别);0 in holes 是 false、0 in reals 是 true,这才是分辨它俩的钥匙。跑完这段我才真正在脑子里刻下:判断一个数组"有没有真实的、能被逐个处理的内容",别看它的 length(那只是声称的规模)、也别看 arr[i](空位也返回 undefined),要看 in(那个位置是不是真的存在元素)。看着两个 length 相同的数组在 forEach 下跑出 0 和 3 这两个截然不同的次数,比记任何"用 Array.from 别用 new Array"的口诀,都更让我对"声称的规模不等于真实的内容"刻骨铭心。
这件事过后,我把项目里所有 new Array(n) 后面接 map/forEach 的地方都搜了一遍,果然又揪出两处:一处想初始化一组默认行、map 没跑导致渲染出一片空白,另一处 fill({}) 让一批本该独立的状态对象共享了同一个引用、改一个全变。我把它们分别换成了 Array.from({length:n}, ...) 和 Array.from(..., () => ({}))。那种把"看着有、其实空"和"看着独立、其实共享"两类隐患一次清干净的踏实,是这次 map 不执行换来的最实在的回报。
更让我警觉的,是这类坑骗人的方式:它不报错,反而表现得一切正常——数组确实造出来了、length 确实是对的、取值也确实给你个 undefined,所有表面信号都在说没问题,唯独你真正想做的事(给每项赋值)悄无声息地没发生。这种沉默的落空,比直接报错难发现得多。从此我对任何看着成功、却没产生预期效果的操作都多一个心眼:它是真做了,还是只是看起来做了。
我也借这次机会在团队里立了条小约定:需要按长度生成并初始化数组,一律用 Array.from({length:n}, fn),禁止 new Array(n).map;需要一组独立对象/状态,一律 Array.from(..., () => ({})),禁止 fill 同一个对象。两条小约定,挡掉的可能是日后某次渲染出一片空白、或一批状态诡异联动的线上 bug。把自己踩过的坑沉淀成别人不必再踩的规则,大概是复盘最值得做的那部分。
说到底,这次不过是把 new Array(5).map 换成 Array.from、加深了对一个语言细节的理解,可它让我真正记住的,是别用一个东西对外的标签去推断它内在的真实。数组的 length、对象声称有的字段、接口说会返回的结构——这些都是声称,不是保证;真要拿它们做事,得先确认那份声称背后,真有对应的实质内容在。这份不轻信表面声称的习惯,比记住任何一个具体 API 都受用。
如今再写下要按数量批量造点什么的代码,我会先在心里确认一句:我造出来的,是 n 个真实可操作的东西,还是 n 个只占着位置、却空无一物的窟窿?就这一句不起眼的确认,常常就是一段能正常跑出结果的代码,和一段看着没报错、却悄悄什么都没做的代码之间,全部的距离。
毕竟,程序不会因为一个数组的 length 写着 5 就对它客气;它只对那些真实存在的元素动手。我能做的,就是在动手之前,先把声称的长度,变成真实的内容。
这件小事也让我对 JavaScript 这门语言多了一分敬畏:它有太多这种取值给你 undefined、却藏着完全不同语义的角落——空位与 undefined、缺失字段与 null、未声明与已声明未赋值。它们都用同一张 undefined 的面孔示人,行为却各不相同。摸清这些角落、知道用 in 而非取值去辨别它们,是写出可靠 JS 代码绕不开的功课。
写在最后
回头看,这场由"稀疏数组空位"引发的"map 不执行"事故,真正教给我的,远不止"用 Array.from 代替 new Array().map"这一个技巧。它让我对"我们常常被一个东西'外在的、声明出来的'属性(比如'它说自己有多大、有多少')所迷惑, 而误以为它'内部真的有那么多实实在在的内容'; 可'声称的规模'和'真实拥有的内容'是两回事——前者可能只是个挂出来的数字, 后者才是你真正能拿来操作的东西",有了一次刻骨的体会。我栽跟头,是因为我把一个东西'对外宣称的规模(length=5)', 当成了'它内部真实存在的内容(5 个元素)'——我看到 new Array(5) 的 length 是 5, 就以为里面实实在在地有 5 个东西等我处理;我没意识到, 这个 5 只是它"声明的长度", 而它内部其实"空空如也"——那 5 个位置是不存在的空位; 当 map 这种"只处理真实存在之物"的操作面对它时, 发现根本没有东西可处理, 自然一次也不跑;而 arr[i] 取值时返回的 undefined, 又给了我一种"这里有个(空的)东西"的错觉, 把我骗得更深。这让我领悟到一个关于"声称的规模与真实的内容"的深刻认知:一个事物"对外声明/表现出的规模、数量、容量", 和它"内部真实拥有、可被实际操作的内容", 是两个不同的东西; 前者是个标签、一个数字, 后者才是实质;很多操作(尤其是那些"逐个处理真实内容"的操作)只认后者——它们对着"声称有、实则空"的东西, 会无声地什么都不做, 而那个"声称的规模"还在那里, 让我们误以为内容俱在;所以当我要"逐项处理"一个东西的内容时, 不能只看它"声称有多少", 而要确认那些内容是否"真实存在、真的在那里"——必要时先把它"填实"(让声称的规模对应上真实的内容), 再去操作。这给了我一种看待"一切'依据某物声称的规模去逐项处理它'之事"时的清醒:每当我准备"遍历/逐项处理"一个东西、并依赖它"声称的大小/数量"时, 要追问"它声称的这个规模, 对应着真实存在的内容吗?还是只是个挂着的数字、里面其实是空的?我这个'逐项处理'会不会因为内容并不真实存在而悄悄落空?"——先确认"真实内容"而非轻信"声称规模", 需要时先填实再操作;"分清声称的规模与真实的内容、先确保内容真实存在再逐项操作", 是用对数组、也是稳妥处理一切'按规模遍历'之事的关键。认清 new Array(N) 是空位不是真实元素、map 跳过空位、要 Array.from 填实再操作——这,是我用一次 map 不执行的事故,换来的、关于 JavaScript、也关于如何分清声称规模与真实内容的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次想 new Array(n).map 批量初始化数组时,先想想"这里面是真有 n 个元素,还是 n 个会被 map 跳过的空位?",并换上 Array.from({length:n}, ...),那我对着那个"map 一次没执行、数组还是空的"折腾的大半天,就值了。
—— 别看了 · 2026