侧边栏壁纸
博主头像
一笑痕

人生若只如初见,
是可喜亦或者是可悲?

  • 累计撰写 178 篇文章
  • 累计收到 7 条评论

new Array(3) 建出来的三个位置,一个键也没有

2026-10-12 / 0 评论 / 9 阅读

new Array(3) 建出来的三个位置,一个键也没有

一段按长度初始化出来的数组,长度显示 3,拿去 forEach 累加,回调一次都没进。a[0] 打出来是 undefined,Object.keys(a) 却是空数组。第一反应是里面装着 undefined,验证之后不是:真正写了 undefined 的三个位置,键是在的,遍历会进去三次。两种数组打印出来长得一模一样。

差别在「键有没有被建立」这件事上。new Array(3) 只把 length 设成 3,没有建任何键,这种数组在规范里叫稀疏数组,那些位置叫洞。[undefined, undefined, undefined] 是三个真实的键,值恰好是 undefined。后面所有分叉都从这里来。

长度写着 3,键列表是空的

== 1. new Array(3) 造出来的三个位置,键一个都没有 ==
length: 3
Object.keys(a): []
0 in a: false
a.hasOwnProperty(1): false
JSON.stringify(a): [null,null,null]
a.join('-'): "--"
== 2. 显式写 undefined 的数组,键是在的 ==
Object.keys(b): ["0","1","2"]
0 in b: true
JSON.stringify(b): [null,null,null]
两者 JSON 输出相同吗: true

0 in a 为 false,a.hasOwnProperty(1) 也是 false,键列表空着。但 a.join('-') 给出 --,JSON 给出 [null,null,null],这两处都当它是三个空位。同一个数组,问「有几个元素」会答 3,问「有哪几个键」会答没有。

遍历方法直接跳过没有键的位置

== 3. 遍历类方法对洞的态度 ==
a.map 回调执行次数: 0
a.map 结果的键: []
b.map 回调执行次数: 3
a.forEach 回调执行次数: 0
a.filter 回调执行次数: 0
a.find 的返回值: undefined
== 4. 查找类方法对洞的态度 ==
a.indexOf(undefined): -1
a.includes(undefined): true
b.indexOf(undefined): 0
a.lastIndexOf(undefined): -1

map、forEach、filter 的回调执行次数全是 0,find 返回 undefined。对比第二组:同一份数据显式写成三个 undefined,map 的回调执行三次。所以一段「遍历不到任何东西」的代码,问题可能不在回调和数据,而在数组本身没建键。

查找类方法的分裂更有意思。a.indexOf(undefined) 返回 -1,a.includes(undefined) 返回 true,两个方法对同一件事给出相反答案。原因是 indexOf 按 === 逐键比,没有键的位置它根本不看;includes 在遍历时把洞当成 undefined 取值。

展开与 JSON 都会把洞填上

== 5. 展开与 Array.from 会把洞填上 ==
Object.keys([...a]): ["0","1","2"]
[...a] 是否等于 b: true
Array.from(a, () => 7): [7,7,7]
a.fill(0) 之后的键: ["0","1","2"]
== 6. 过一遍 JSON,洞变成真的 null ==
JSON.parse 之后 Object.keys: ["0","1","2"]
JSON.parse 之后 0 in 结果: true
JSON.parse 之后 map 回调次数: 3

[...a] 得到三个真的 undefined,Object.keys 是 ["0","1","2"],和显式写出来的数组相等。Array.from(a, () => 7) 直接给三个 7。JSON 更彻底:序列化时洞写成 null,读回来是三个真实的 null,0 in 结果 为 true,map 照常进三次。

这条对排查有用。若数组在某个环节过了一次 JSON 或者一次展开,洞就没了,前面看到的「遍历不进回调」在后半程复现不出来。要复现得回到数据刚生成的那一步。

delete 与 length 是两件事

== 7. 长度只增不减:删掉中间一项之后 ==
delete 之后 length: 3
delete 之后 Object.keys: ["0","2"]
delete 之后 JSON.stringify: [1,null,3]
把 length 写成 5 之后的键: ["0","2"]

delete a[1] 只把键 1 删掉,length 还是 3,JSON 变成 [1,null,3]。把 length 改成 5 只是把长度声明改大,键列表仍然是 ["0","2"],中间那些位置还是洞。长度在这套规则里更接近一个声明,不是「有几个元素」的答案。

这种数组平时从哪来

常见的来源有几处。按长度开数组再逐个赋值,中间某几个位置没走到;delete 掉一个元素之后继续用同一个数组;从别处拷贝时只拷贝了一部分下标。第三种最隐蔽,数据看着是完整的三行,实际只有两行有键。

要建一个每项都到位的数组,Array.from({length: n}, fn) 和 new Array(n).fill(0) 都是干净的写法,后一种记得先 fill 再改,直接给 new Array(n) 的每一项赋值也可以,但别漏项。真正需要「这几位还不存在」的语义时,稀疏数组就是它本身的表达,只是别让它悄悄流过 JSON、展开和遍历这三道关。

判断手上这个数组是不是稀疏的,有个不用猜的办法:Object.keys(a).length 和 a.length 对不上就是。另外数组从接口回来时如果带着 null 占位,也要先确认是值真的为 null,还是某个环节把它写成了洞,两种情况在后面几条路上的行为不一样,前者遍历时会进回调,后者不会。

这一条我倾向于在函数入口就把数组收成实心的,中间少一种状态要判断。代价是长度固定时多一次 fill,通常不值得为省这一次去留洞。

    🤞 分享