借用数组非破坏性方法来优化react的状态更新

背景

我们在写react项目中 使用到会更改原始数组的方法的时候,经常需要 使用[...arr]来拷贝一份,经常使用到的比如 sort reverse splice 等等,例如

js 复制代码
const handleSort = () => {
  // 先拷贝原数组,再排序(因为 sort() 会修改原数组)
  const newNumbers = [...numbers].sort((a, b) => a - b);
  setNumbers(newNumbers);
};

我们可以借助非破坏性方法来避免此类复制操作,比如 toSorted toReversed() toSpliced(start, deleteCount, ...items) with(index, value)

用法介绍

JavaScript 数组引入了几个新的方法,这些方法都是非破坏性 的,即不会修改原数组,而是返回一个新的修改后的数组。这与原有的 sort()reverse()splice() 等方法不同,后者会直接修改原数组。

  1. toSorted()

    • 功能:返回一个新的 sorted 数组,类似于 sort() 但不修改原数组
    • 参数:可选的比较函数
  2. toReversed()

    • 功能:返回一个新的 reversed 数组,类似于 reverse() 但不修改原数组
    • 无参数
  3. toSpliced(start, deleteCount, ...items)

    • 功能:返回一个新的 splice 后的数组,类似于 splice() 但不修改原数组
    • 参数:与 splice() 相同
  4. with(index, value)

    • 功能:返回一个在指定索引处更新了值的新数组
    • 参数:要更新的索引和新值

用法及输出

js 复制代码
// 原始数组
const numbers = [3, 1, 4, 2];
const fruits = ['apple', 'banana', 'cherry', 'date'];

// 1. toSorted() - 非破坏性排序
const sortedNumbers = numbers.toSorted();
console.log('原数组:', numbers); // [3, 1, 4, 2]
console.log('排序后:', sortedNumbers); // [1, 2, 3, 4]

// 使用比较函数
const sortedByLength = fruits.toSorted((a, b) => a.length - b.length);
console.log('按长度排序:', sortedByLength); // ['date', 'apple', 'banana', 'cherry']

// 2. toReversed() - 非破坏性反转
const reversedNumbers = numbers.toReversed();
console.log('原数组:', numbers); // [3, 1, 4, 2]
console.log('反转后:', reversedNumbers); // [2, 4, 1, 3]

// 3. toSpliced() - 非破坏性splice
// 从索引1开始删除1个元素,并插入"blueberry"
const modifiedFruits = fruits.toSpliced(1, 1, 'blueberry');
console.log('原数组:', fruits); // ['apple', 'banana', 'cherry', 'date']
console.log('修改后:', modifiedFruits); // ['apple', 'blueberry', 'cherry', 'date']

// 4. with(index, value) - 更新指定索引的值
const updatedNumbers = numbers.with(2, 10);
console.log('原数组:', numbers); // [3, 1, 4, 2]
console.log('更新后:', updatedNumbers); // [3, 1, 10, 2]

// 这些方法可以链式调用
const result = numbers
  .toSorted()          // [1, 2, 3, 4]
  .toReversed()        // [4, 3, 2, 1]
  .with(0, 5)          // [5, 3, 2, 1]
  .toSpliced(2, 0, 6); // [5, 3, 6, 2, 1]
  
console.log('链式调用结果:', result);

万能的 toSpliced

js 复制代码
const arr = [1, 2, 3];

// 模拟 push('末尾') → 在索引 3 处添加(超出原数组长度则自动加在末尾)
arr.toSpliced(arr.length, 0, '末尾'); // [1, 2, 3, '末尾']

// 模拟 unshift('开头') → 在索引 0 处添加
arr.toSpliced(0, 0, '开头'); // ['开头', 1, 2, 3]

// 模拟 splice(1, 1, '替换') → 从索引1删除1个元素,插入新值
arr.toSpliced(1, 1, '替换'); // [1, '替换', 3]

toSplicedwith 以及with能否替换 toSpliced

with 方法和 toSpliced 虽然都能修改数组元素,但它们的适用场景不同,不能完全用 with 替代 toSpliced 来模拟 splice(1, 1, '替换') 这种操作

具体区别:

with(index, value) 的核心作用是 「替换指定索引的元素」 (仅修改,不增删长度),而 splice(1, 1, '替换')「先删除元素,再插入新元素」(可能改变数组长度)。

举例说明:

假设原数组为 [1, 2, 3]

js 复制代码
const arr = [1, 2, 3];
const newArr = arr.toSpliced(1, 1, '替换'); 
// 结果:[1, '替换', 3]
// 过程:删除索引1的元素(2),插入'替换',数组长度不变(仍为3)
ini 复制代码
const arr = [1, 2, 3];
const newArr = arr.with(1, '替换'); 
// 结果:[1, '替换', 3]
// 过程:直接将索引1的元素(2)替换为'替换',数组长度不变

这两种方式在这个例子中结果相同,但本质不同:

  • toSpliced 是「删除 + 插入」的组合操作(先删后加)
  • with 只是单纯的「替换」操作(直接覆盖)
js 复制代码
const arr = [1, 2, 3, 4];

// 用 toSpliced 实现:删除索引1开始的2个元素,插入'x','y'
const newArr1 = arr.toSpliced(1, 2, 'x', 'y'); 
// 结果:[1, 'x', 'y', 4](长度不变)

// 用 toSpliced 实现:只删除不插入(长度减少)
const newArr2 = arr.toSpliced(1, 2); 
// 结果:[1, 4](长度从4变为2)

// 用 toSpliced 实现:只插入不删除(长度增加)
const newArr3 = arr.toSpliced(1, 0, 'x', 'y'); 
// 结果:[1, 'x', 'y', 2, 3, 4](长度从4变为6)

以上场景中,with 完全无法实现,因为它:

  • 只能修改单个索引的元素
  • 不能删除或插入元素(无法改变数组长度)
  • 一次只能处理一个元素

react更新变量时 原有的 sort()reverse()splice() 等方法为何需要拷贝

在 React 中,状态(state)是不可直接修改的(immutable)。当你需要更新数组状态时,必须创建一个新的数组实例,否则 React 可能无法检测到状态变化,导致组件不重新渲染。

过去,为了遵循这种不可变更新原则,开发者通常会使用扩展运算符(...)手动拷贝数组,再进行修改。而(toSortedtoReversedtoSplicedwith)本身就会返回新数组,完美契合 React 的状态更新需求,因此不再需要手动拷贝

原始写法

js 复制代码
const [numbers, setNumbers] = useState([3, 1, 4, 2]);

// 排序数组并更新状态
const handleSort = () => {
  // 先拷贝原数组,再排序(因为 sort() 会修改原数组)
  const newNumbers = [...numbers].sort((a, b) => a - b);
  setNumbers(newNumbers);
};

// 修改指定索引的值
const handleUpdate = () => {
  // 先拷贝原数组,再修改值
  const newNumbers = [...numbers];
  newNumbers[2] = 10;
  setNumbers(newNumbers);
};

新方法写法及原因

js 复制代码
const [numbers, setNumbers] = useState([3, 1, 4, 2]);

// 排序数组(toSorted 返回新数组,不修改原数组)
const handleSort = () => {
  setNumbers(numbers.toSorted((a, b) => a - b));
};

// 修改指定索引的值(with 直接返回新数组)
const handleUpdate = () => {
  setNumbers(numbers.with(2, 10));
};

// 反转数组(toReversed 返回新数组)
const handleReverse = () => {
  setNumbers(numbers.toReversed());
};
  • React 状态更新依赖「引用变化」来检测更新:只有当 setState 传入的新值与旧值引用不同时,组件才会重新渲染。
  • 旧方法(sortreversesplice)会修改原数组(引用不变),因此必须先用 ... 拷贝出一个新数组再操作。
  • 新方法(toSorted 等)天生返回新数组(引用不同),无需手动拷贝,直接传入 setState 即可触发更新。

错误示范

js 复制代码
const [numbers, setNumbers] = useState([3, 1, 4, 2]);

// 错误写法
const handleSort = () => {
  // 直接排序:sort() 会修改原数组,且返回原数组引用
  const newNumbers = numbers.sort((a, b) => a - b);
  
  // 此时 numbers 和 newNumbers 是同一个数组(引用相同)
  console.log(numbers === newNumbers); // true
  
  setNumbers(newNumbers); // React 会认为状态没变化,不重新渲染
};

执行后你会发现:

  • 原数组 numbers 已经被修改(违反不可变性)
  • 组件可能不会重新渲染(因为引用没变)

结尾

愿借助 toSorted toReversed() toSpliced(start, deleteCount, ...items) with(index, value) 实现更少的代码 更好的划水

相关推荐
前端大卫6 小时前
Vue3 + Element-Plus 自定义虚拟表格滚动实现方案【附源码】
前端
却尘6 小时前
Next.js 请求最佳实践 - vercel 2026一月发布指南
前端·react.js·next.js
ccnocare6 小时前
浅浅看一下设计模式
前端
Lee川6 小时前
🎬 从标签到屏幕:揭秘现代网页构建与适配之道
前端·面试
Ticnix6 小时前
ECharts初始化、销毁、resize 适配组件封装(含完整封装代码)
前端·echarts
纯爱掌门人6 小时前
终焉轮回里,藏着 AI 与人类的答案
前端·人工智能·aigc
twl7 小时前
OpenClaw 深度技术解析
前端
崔庆才丨静觅7 小时前
比官方便宜一半以上!Grok API 申请及使用
前端
星光不问赶路人7 小时前
vue3使用jsx语法详解
前端·vue.js
天蓝色的鱼鱼7 小时前
shadcn/ui,给你一个真正可控的UI组件库
前端