告别sort()原罪:JavaScript不可变数组方法完全指南与状态管理实战

前几天在一个React组件里写了这样一句:const sorted = items.sort((a,b) => b.score - a.score);,结果页面自动刷新后,原本倒序排列的列表居然不重新渲染了。排查半天才发现,sort 直接修改了原数组,而React认为引用没变就不触发更新。那一刻我意识到,虽然用了很多年JavaScript,但有时候还是会在“原地修改”这件事上栽跟头。

好在ES2023带来了一组新的数组方法:toSorted()、toReversed()、toSpliced() 和 with()。它们做的事情和 sort、reverse、splice、索引赋值一样,但区别在于——它们返回新数组,原数组纹丝不动。这一下就解决了状态变更检测、函数式编程、以及很多因为副作用产生的隐蔽bug。这篇文章就结合几个实际场景,把这几个方法彻底讲透。

原地修改有多痛

看一个简单的例子:

const original = [3, 1, 2];
const sorted = original.sort();
console.log(sorted); // [1,2,3]
console.log(original); // [1,2,3] 原数组被改了

如果你在某个函数内部对传入的数组调用了 sort(),外部的数组也会跟着变,这会引发难以追踪的数据污染。在React、Vue这类依赖引用对比的框架里,还可能导致视图更新失败。以前我们只能手动复制一份:[...arr].sort() 或者 arr.slice().reverse(),但多出来的展开或切片操作既啰嗦又容易忘记。

新方法的命名以 to 开头,清晰地表达了“返回一个新数组”的意图。它们的浏览器支持也相当不错,Chrome 110+、Edge 110+、Firefox 115+、Safari 16.4+ 都已经就绪。对于旧环境,core-js 也提供了对应的 polyfill。

toSorted():安全的排序

用法和 sort() 几乎一样,接收一个比较函数,返回排好序的新数组:

const scores = [45, 12, 89, 33];
const sortedAsc = scores.toSorted((a, b) => a - b);
const sortedDesc = scores.toSorted((a, b) => b - a);

console.log(scores);      // [45, 12, 89, 33] 原数组不变
console.log(sortedAsc);   // [12, 33, 45, 89]
console.log(sortedDesc);  // [89, 45, 33, 12]

如果不传比较函数,它和 sort() 一样按照字符串的 Unicode 码点排序,但同样不会修改原数组。在我那个React例子中,只需要把 items.sort(...) 改成 items.toSorted(...) 再赋值给新状态,所有问题就消失了。

toReversed():反转顺序

reverse() 对应,返回一个逆序的新数组:

const letters = ['a', 'b', 'c', 'd'];
const reversed = letters.toReversed();

console.log(letters);  // ['a','b','c','d']
console.log(reversed); // ['d','c','b','a']

需要倒序展示消息列表或者时间线时,这个方法可以直接用在渲染逻辑中,不用担心搞乱原始数据源。以往我们得先 [...data].reverse(),现在代码读起来更直接。

toSpliced():无副作用的增删改

splice() 是一个典型的“多功能但危险”的方法,它会直接修改原数组。而 toSpliced() 使用完全相同的参数,却返回一个新数组:

const todos = ['阅读', '写代码', '运动', '学习'];
// 删除索引1开始的2个元素,并插入'休息'
const updated = todos.toSpliced(1, 2, '休息');

console.log(todos);   // ['阅读', '写代码', '运动', '学习']
console.log(updated); // ['阅读', '休息', '学习']

想要在数组中间插入新项目?用 toSpliced(start, 0, newItem),删除0个元素。想要删除一个项目而不留空洞?用 toSpliced(index, 1)。这些操作在管理不可变状态时极其顺手,稍后的案例中会大量用到。

with():替换单个元素

有时候我们只想修改数组里某一个位置的值。以前可能是 arr[2] = '新值' 或者用 map,现在 with() 方法提供了最简练的方式:

const colors = ['红', '绿', '蓝'];
const newColors = colors.with(1, '黄');

console.log(colors);    // ['红', '绿', '蓝']
console.log(newColors); // ['红', '黄', '蓝']

它支持负数索引,arr.with(-1, '最后') 会替换最后一个元素。相比 toSpliced(index, 1, newVal)with() 的语义更加明确:我只想替换这一个位置,别的事不做。在和React的 setState 搭配时,这种方法写起来非常舒服。

实战:构建不可变的待办列表状态

下面我们把四个方法组合起来,实现一个没有副作用的待办列表管理。场景很简单:一个任务数组,我们需要完成、添加、删除、批量排序和反转功能,但任何操作都不能破坏原始数据。

let tasks = [
  { id: 1, title: '学习JavaScript新特性', done: false },
  { id: 2, title: '写技术文章', done: true },
  { id: 3, title: '运动30分钟', done: false },
  { id: 4, title: '回复邮件', done: true }
];

// 1. 标记完成任务(用with替换指定位置的对象)
function markDone(tasks, taskId) {
  const index = tasks.findIndex(t => t.id === taskId);
  if (index === -1) return tasks;
  return tasks.with(index, { ...tasks[index], done: true });
}

// 2. 添加新任务(用toSpliced在末尾插入)
function addTask(tasks, title) {
  const newTask = {
    id: Date.now(),
    title,
    done: false
  };
  return tasks.toSpliced(tasks.length, 0, newTask);
}

// 3. 删除任务(用toSpliced移除指定位置)
function removeTask(tasks, taskId) {
  const index = tasks.findIndex(t => t.id === taskId);
  if (index === -1) return tasks;
  return tasks.toSpliced(index, 1);
}

// 4. 按标题排序(用toSorted)
function sortByTitle(tasks) {
  return tasks.toSorted((a, b) => a.title.localeCompare(b.title, 'zh'));
}

// 5. 反转任务顺序(用toReversed)
function reverseTasks(tasks) {
  return tasks.toReversed();
}

// 测试:依次执行操作
console.log('原始任务:', tasks);

let updated = markDone(tasks, 3);
updated = addTask(updated, '读书一小时');
updated = removeTask(updated, 2);
updated = sortByTitle(updated);
updated = reverseTasks(updated);

console.log('更新后任务:', updated);
console.log('原始任务是否变化:', tasks === updated); // false
console.log('原始任务仍为初始状态:', tasks);

可以看到,整个过程中 tasks 始终没有改变,每次操作都返回新的数组。这样的代码非常容易集成到React的 useState 或Redux的reducer中,因为每次都会产生新的引用,框架可以准确感知变化。

兼容性与过渡期的考虑

如果项目需要支持较旧的浏览器,可以使用core-js的polyfill,或者在构建工具中配置 @babel/preset-env 加上 shippedProposals。在日常开发中,尽早切换到这些新方法,带来的不仅是代码清晰度的提升,也能减少深拷贝和展开运算符的使用频率,对性能也有一定好处。

总结

这几个新数组方法并不是什么颠覆性的技术,但它们在日常编码中消除了一类非常常见的错误源。尤其是在前端状态管理、函数式编程和数据处理链中,“不改变原数组”这种保证可以让代码的推理难度下降一个档次。下次再需要对数组排序、反转、删除或替换时,试试以 to 开头的那几个方法,你或许会发现,很多以前需要小心翼翼复制数组的场景,现在一行就解决了,并且再也不用担心误改数据源的隐患。

告别sort()原罪:JavaScript不可变数组方法完全指南与状态管理实战
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请联系本站我们会及时删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 javascript 告别sort()原罪:JavaScript不可变数组方法完全指南与状态管理实战 https://www.taomawang.com/web/javascript/2154.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务