
new Array(1).toSorted() 看起来只是把一个空数组排了序,结果却不再是原来那种空槽。读取下标得到的仍是 undefined,length 仍是 1,普通打印也不容易看出变化;但下标已经成为结果数组的自有属性。对后续遍历而言,这不是外观差异,map 终于会进回调。
下标有值和下标存在是两回事
空槽不是显式放进去的 undefined。new Array(1) 只设置长度,没有创建索引 0。所以 0 in array 和 Object.hasOwn(array, 0) 都是 false,尽管直接读 array[0] 时结果仍为 undefined。显式写成 [undefined] 则创建了索引,两个检查都会返回 true。
这两种状态过去后容易被压成同一种说法,因为取值相同;但某些数组方法会根据索引是否存在决定是否处理该位置。稀疏数组调用 map,空槽会被跳过;显式的 undefined 会进入回调。于是只记录数组内容、没记录属性结构时,调试信息可能把问题藏起来。
toSorted 返回的是一份实心副本
本机用 Node.js v26.8.1 跑了单元素空槽、显式 undefined,以及混有数字的稀疏数组。输出显示,空槽调用 toSorted 后,结果的 Object.keys 包含 0,0 in result 为真,Object.hasOwn(result, 0) 也为真。对这个结果调用 map,回调执行一次。索引里放的值是 undefined,只是它从空槽变成了确实存在的属性。
混合数组把顺序看得更清楚:原来位于中间的洞,排序后成为末尾的 undefined,结果所有索引都存在。原数组的键仍只有两个,toSorted 没有动它。所谓复制在这里不只是换一个数组引用,也重新构造了每个位置的属性状态。
证据中的 <hole> 是验证脚本为缺失自有索引加的标记,不是 JavaScript 返回的特殊值。JSON.stringify 会把空槽和 undefined 数组项都表示成 null,无法单靠序列化结果判断两者是否相同;因此脚本逐项查看 Object.hasOwn,再用 Object.keys 和 in 交叉确认。这样的检查比在控制台里看一行数组字面量更能回答结构问题。
node: v26.8.1
source hole: ["<hole>"]
source hole keys: []
source hole 0 in array: false
source hole hasOwn(0): false
source undefined: ["undefined"]
source undefined keys: ["0"]
source undefined 0 in array: true
source undefined hasOwn(0): true
toSorted hole result: ["undefined"]
toSorted hole result keys: ["0"]
toSorted hole result 0 in array: true
toSorted hole result hasOwn(0): true
toSorted undefined result: ["undefined"]
toSorted undefined result keys: ["0"]
toSorted undefined result 0 in array: true
toSorted undefined result hasOwn(0): true
hole source unchanged: true
result is a new array: true
hole result value is undefined: true
hole result owns index 0: true
map callbacks on toSorted hole result: 1
sort result: ["1","3","<hole>"]
sort result keys: ["0","1"]
sort result 0 in array: true
sort result hasOwn(0): true
sort returns source: true
mixed source: ["3","<hole>","1"]
mixed source keys: ["0","2"]
mixed source 0 in array: true
mixed source hasOwn(0): true
mixed toSorted result: ["1","3","undefined"]
mixed toSorted result keys: ["0","1","2"]
mixed toSorted result 0 in array: true
mixed toSorted result hasOwn(0): true
mixed source keys after sorting: ["0","2"]
和 sort 的差别不止原数组会不会变
同一组 [3, , 1] 调用 sort,数组本身会被修改并返回自身;排序后两个数字占据前面的位置,末尾依然是洞,Object.keys 只有 0 和 1。换成 toSorted,新数组的键是 0、1、2,最后一格是真实的 undefined。这组对照说明,toSorted 的复制语义不会保留洞。
实际代码若把数组传给依赖稀疏结构的函数,这一处转换就会改变回调是否执行。反过来,若后续逻辑要求每个下标都能被遍历到,toSorted 的结果更直白,不必把复制和填洞拆成两次操作。但若空槽承载「尚无这个索引」的含义,不能把 toSorted 当成无损副本;先看 Object.hasOwn,不要只比较 length、下标值或 JSON 字符串。
这不是说排序结果里的 undefined 会参与比较器。单元素测试并不能验证比较器的调用路径,因此这里不把它当成证据;这次验证只关心最终结构。可复现的部分是空槽来源没有属性,排序副本却有属性,而且结果上的 map 确实访问了该位置。把结论限定在这些可观察值上,足以判断是否发生了填实,不必借助控制台的展示格式猜内部状态。
我更愿意把稀疏数组当作需要显式维护的结构,而不是普通列表的一种省内存写法。排完序后属性状态可能变了,源数组却还保留旧状态,排查时两份数组看上去几乎一样,实际经过的语义已经分开。需要区分空槽和 undefined 时,验证属性存在性;只关心排序后的值时,toSorted 返回的密集结果才是更省心的选择。