在 JavaScript 中,原生 sort() 方法会原地修改原数组。本文详解如何利用 ES6 扩展运算符 [...arr].sort() 以及 ES2023 原生 toSorted() 方法实现非破坏性排序,剖析内存原理、代码示例、运行环境兼容性与浅拷贝避坑指南。
JS数组排序不修改原数组:使用扩展运算符或toSorted()方法
时间:2026-11-12
浏览:125
文章目录
arr.sort() 会在原内存空间上修改数组本身(原地排序),并返回原数组的引用。如果你需要保留原数组内容不变,业界主流有两种标准解决路径:一种是 ES6 兼容度最高的扩展运算符浅拷贝 [...arr].sort(),另一种是 ES2023 新增的原生非破坏性方法 arr.toSorted()。本文将具体阐明两者背后的运作机理、代码实现与选型场景。
一、为什么原生的 sort() 会改变原数组
JavaScript 的 Array.prototype.sort() 自诞生起就被设计为一种原地操作(in-place)的方法。这意味着调用它时,排序算法直接在原数组所分配的堆内存槽位中重新排列元素,并不会分配新的数组内存。
我们可以通过一段极简的代码观察这种破坏性特征:
// 原生 sort() 的破坏性示例
const original = [40, 10, 30];
const returned = original.sort((a, b) => a - b);
console.log("原数组 original:", original);
console.log("返回值 returned:", returned);
console.log("引用是否同一:", original === returned);
可以看到,original 本身的元素排列已经被彻底打乱,而且排序方法的返回值与原数组指向同一个堆内存地址。
在现代前端开发中(例如 React 的 useState、Redux 状态流或 Vue 的计算属性),原地修改数据往往是隐藏 Bug 的高发区。当你在组件内部直接调用 sort() 修改了 props 或 state 时,状态引用没有变化,不仅会导致组件无法正确响应更新,还可能污染全局或上层组件共享的数据源。因此,“排序时绝不改动原数组”是编写高质量纯函数与响应式代码的基本要求。
二、方案一:使用 ES6 扩展运算符 [...arr].sort()
在 ES2015(ES6)及以上环境中,最常用且最广泛兼容的做法是先使用数组扩展运算符(Spread Syntax)创建一份浅拷贝,再在拷贝出的新数组上调用 sort()。
1. 具体操作语法
// 使用扩展运算符实现非破坏性排序
const scores = [85, 92, 78, 99, 64];
// 步骤:先用 [...scores] 浅拷贝新数组,再调用 .sort()
const sortedScores = [...scores].sort((a, b) => a - b);
console.log("原数组 scores:", scores);
console.log("排序后副本 sortedScores:", sortedScores);
console.log("内存引用比较:", scores === sortedScores);
2. 原理与执行推导
这行简洁的代码实际上经历了明确的三步演变:
- 解构浅拷贝:
[...scores]会在内存中申请一个全新的数组实例,并将原数组的第一层元素逐一拷贝进新容器中。 - 副本重排:此时链式调用的
.sort((a, b) => a - b)虽然依然是原地修改,但它所修改的只是第一步创建出的临时新数组,原数组scores已经被完全隔离在操作范围之外。 - 安全交付:最终赋值给
sortedScores的是经过排序的全新数组实例,两者的堆内存指针完全独立。
值得一提的是,在没有扩展运算符的早期代码中,开发者常用 arr.slice().sort() 或 [].concat(arr).sort()。这两种方式与 [...arr].sort() 的底层逻辑完全一致,都是通过创建浅拷贝来阻隔 sort() 的原地副作用。而在日常代码中,扩展运算符的书写最为直观、认知负荷最低。
三、方案二:使用 ES2023 新增的 toSorted() 方法
为了彻底解决数组变异方法(Mutating Methods)给函数式编程带来的不便,ECMAScript 2023(ES14)正式引入了 Change Array by Copy 规范提案,为数组增添了一组天然具备“非破坏性”的原生方法,其中专门用于排序的即为 Array.prototype.toSorted()。
1. 具体操作语法
// 使用 ES2023 原生 toSorted() 方法
const temperatures = [28, 19, 32, 25, 21];
// 直接调用 toSorted(),无需提前手动解构拷贝
const sortedTemps = temperatures.toSorted((a, b) => a - b);
console.log("原数组 temperatures:", temperatures);
console.log("排序结果 sortedTemps:", sortedTemps);
console.log("是否产生新实例:", temperatures !== sortedTemps);
2. toSorted() 的核心特性
与传统的 sort() 相比,toSorted() 的主要特质体现在:
- 单步原子操作:不再需要
[...]展开语法作为中间步骤,代码表达清晰直接,语义就是“返回此数组排序后的副本”。 - 入参完全一致:
toSorted()接收与sort()相同的比较函数compareFn。若传入相同的比较逻辑,两者输出的排列顺序严格一致。 - 更易链式调用:在复杂的数据处理流中,可以直接
items.filter(...).toSorted(...).map(...),而无需在中间阶段用中括号打断链条。
四、两种方案的关键差异与选型依据
读者在实际开发中可能会产生疑问:既然两种方式都能保护原数组,那么在日常项目中应该如何抉择?我们从运行环境、语法规范与底层行为三个维度进行比对。
| 特性维度 | 扩展运算符 [...arr].sort() | 原生 toSorted() 方法 |
|---|---|---|
| 语言规范 | ECMAScript 2015 (ES6) | ECMAScript 2023 (ES14) |
| Node.js 支持 | Node.js 6.0+(全版本覆盖) | Node.js 20.0.0+(高版本支持) |
| 主流浏览器支持 | Chrome 46+, Safari 10+, Firefox 16+ | Chrome 110+, Safari 16.4+, Firefox 115+ |
| 执行步骤 | 语法级解构浅拷贝 + 链式调用修改 | 引擎级单次内置“复制并排序” |
| 旧环境适配 | 无需 Polyfill,直接运行 | 需要引入 core-js 或 Babel 插件补齐 |
选型建议:
- 新项目或现代服务端(Node.js >= 20):建议优先采用
toSorted()。代码意图直接、无解构样板,符合现代 ECMAScript 编码规范。 - 旧项目维护或需要兼顾旧版客户端/移动端 Webview:优先选用
[...arr].sort()。它是跨平台兼容性最好的通用模式,在任何支持 ES6 的环境中均能稳定工作。
五、排序操作的两大常见陷阱与注意事项
即便成功保护了原数组的顺序,在实际编码中仍有两处极易被忽视的陷阱,需要格外警惕:
(a, b) => a - b无论使用
sort() 还是 toSorted(),如果不传递比较函数,JavaScript 默认会将数组各项转换为 UTF-16 字符串后比较字典顺序。例如:[100, 25, 5].toSorted() 的结果是 [100, 25, 5],因为字符 "1" 小于 "2","2" 小于 "5"。若要进行数值排序,必须始终明确书写 (a, b) => a - b(升序)或 (a, b) => b - a(降序)。
如果数组的元素是对象类型(例如
[{ id: 1, name: 'Alice' }]),无论是扩展运算符还是 toSorted(),复制出来的都仅仅是对象的内存引用地址。虽然排序后数组内部的顺序是独立的,但若直接修改新数组中某个对象的属性(如 sortedList[0].name = 'Bob'),原数组中对应对象的属性同样会被同步修改。
如果你不仅需要重新排列顺序,还需要对对象内部属性做解耦隔离,应当结合深拷贝 API:
// 针对对象数组的深度安全排序示例
const users = [
{ id: 3, profile: { level: 2 } },
{ id: 1, profile: { level: 5 } },
{ id: 2, profile: { level: 1 } }
];
// 借助原生的 structuredClone 进行深拷贝后再排序
const safelySortedUsers = structuredClone(users).sort((a, b) => a.id - b.id);
// 修改新数组对象中的属性,原数组保持绝对隔离
safelySortedUsers[0].profile.level = 999;
console.log("原数组数据:", users[1].profile.level); // 输出 5,不受影响
六、总结
在 JavaScript 中保护原数组不受排序污染,本质上是在修改操作之前切断原有的内存引用关系:
- 使用
[...arr].sort(compareFn):利用解构语法先在堆中复制出独立数组实例,再调用排序。兼容性极佳,适用于各类通用环境。 - 使用
arr.toSorted(compareFn):利用 ES2023 官方引入的复制型方法,直接以表达式形式返回有序新数组。语义精确明了,是现代 JavaScript 开发的推荐标准。
在日常业务中,结合团队的基线运行时版本灵活选用这两种方案,并在数字排序与引用类型场景下牢记显式比较器与浅拷贝边界,即可从根源上杜绝数组原地修改引发的数据副作用。
实用工具推荐
绿化、压缩、噪点和虚拟水印,自由调节做旧程度。
选择画质和输出格式,预览压缩前后的大小。