先记住这个答案
会。Array.prototype.sort() 是原地排序,排序过程中会直接重排原数组的元素,结束后返回的也还是那个数组对象的引用,因此原数组内容已被修改。若希望保留原数组,常见做法是先创建浅拷贝:用 original.slice().sort() 或 [...original].sort(),也可以使用 ES2023 的 original.toSorted()。要注意拷贝只是浅层,当数组元素是对象时,排序后的数组与原数组共享这些对象;若之后通过任一数组修改对象,原数组中对应的对象也会看到改变。
sort()原地重排并修改原数组- 返回值是原数组的同一引用
- 先浅拷贝再排序可保护原数组
sort 修改的是调用它的数组本身
Array.prototype.sort 会把排序结果写回调用它的数组,并返回这个数组本身。因此把返回值赋给另一个变量,并不会得到独立副本;两个变量指向同一数组,后续改动会互相可见。这是 API 的可观察行为,不要求引擎内部完全不分配辅助存储。
默认不传比较器时,元素会先被转成字符串,再按 UTF-16 码元升序排列,例如 [10, 9].sort() 的结果是 [10, 9],因为字符串 "10" 排在 "9" 前面。若希望按数值升序,必须传入 (a, b) => a - b。比较器只决定排序结果,不改变“原地重排”这一核心语义。
展示排序结果但保留原始顺序
一个工单后台按提交时间维护数组 tickets。用户要求点“按优先级排序”时只改变列表显示,原时间顺序不能被覆盖。直接调用 tickets.sort((a, b) => b.priority - a.priority) 会让 tickets 永久失去原顺序;应改为 const view = [...tickets].sort((a, b) => b.priority - a.priority) 并渲染 view。这样原数组仍然是提交时间顺序。
复制外层数组后,展示层可以读取 view,默认顺序继续保存在 tickets 中。恢复默认排序时切回 tickets 即可。这个方案适用于只调整排列顺序的视图:比较器读取优先级但不修改对象;若比较器自己修改对象、访问有副作用的 getter,则需要单独审查这些副作用。
const tickets = [{ id: 'A', priority: 1 }, { id: 'B', priority: 3 }];
const view = [...tickets].sort((a, b) => b.priority - a.priority);
console.log(tickets.map(item => item.id).join(','));
console.log(view.map(item => item.id).join(','));
console.log(view === tickets);
console.log(view[0] === tickets[1]);
view[0].priority = 9;
console.log(tickets[1].priority);查看输出与解释
A,B
B,A
false
true
9普通 JavaScript 环境可运行。外层数组不同,所以顺序互不覆盖;内部工单对象仍被共享,因此通过 view 修改优先级也会改变 tickets 读取到的对象。复制数组与禁止修改对象是两个不同要求。
浅拷贝无法隔离所有变更
原数组是稀疏数组时,复制方式会带来不同结果。例如 const arr = ['a', 'c', , 'b']; 直接 arr.sort() 会把空位移到末尾,结果仍是空位;arr.slice().sort() 同样保留空位;但 [...arr].sort() 会先把空位读成 undefined,排序结果末尾出现显式 undefined。这种差异在普通连续数组上不可见,遇到来自接口或删除操作留下的稀疏数据时必须留意。
浅拷贝只复制数组最外层槽位,不复制元素对象。const original = [{ value: 2 }, { value: 1 }]; const clone = [...original].sort((a, b) => a.value - b.value); 之后,两个数组共享同样的两个对象,只是索引顺序不同。若排序完成后通过 clone[0].value = 99,original 中对应索引的对象也会被改动。要真正隔离对象级变化,需要额外做深拷贝,代价需按场景单独评估。
容易答错的地方
- sort 返回新数组
- 事实是
sort()返回排序后的原数组引用,不是新建数组。可以运行const a = [3, 1]; const b = a.sort(); console.log(b === a);,输出true。若把它当成map那样的新数组使用,后续修改返回值会意外改动原数组。 - 浅拷贝一层就彻底不变
[...arr]只隔离外层数组顺序。如果元素是对象,通过任一数组修改该对象,另一数组也能读到变化。深拷贝能在所支持的数据范围内隔离对象,但不自动建立只读约束;需要防止修改时,应通过数据所有权、不可变更新或合适的冻结策略约束写入。
面试官还会怎么问?
`toSorted` 和先复制再 `sort` 一定等价吗?
在已支持 toSorted 的环境里,对普通连续数组两者通常等价:都返回浅拷贝后的排序结果,原数组不受影响;差别在于 toSorted 直接返回新数组,调用方少写一步。但对稀疏数组二者并不等价:slice().sort() 会保留空位,而 toSorted 按索引读取时会把空位当作 undefined 取出并产出稠密数组,行为与 [...arr].sort() 一致,排序结果末尾是显式 undefined 而非空位。旧环境没有该方法时,用 slice().sort() 或 [...arr].sort() 保证兼容。
`slice` 和 `Array.from` 复制稀疏数组有何不同?
slice 会保留稀疏数组中的空位,Array.from 和展开运算符走迭代器协议,会把空位当作 undefined 取出。若要排序后仍区分 empty 与 undefined,应当优先使用 slice 复制。
在排序前先复制数组会影响性能吗?
会多一次 O(n) 的浅拷贝和额外内存,排序本身仍是主导开销。若需要多次读取同样的排序结果,不要每次重新排序,可缓存拷贝后的数组;若只是临时展示,复制一次的成本通常可以接受。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。