掘金文章归档 · 第 46 篇 React 进阶系列 3/6 数组 · 状态更新

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

toSorted / toReversed / toSpliced / with:免拷贝更新 state

原文作者:随意_(掘金) | juejin.cn/post/7537106747279540270 | 发布于 2025-08-11 | 阅读 167 · 约 5 分钟

0背景

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

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

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

1用法介绍

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

方法功能参数
toSorted()返回一个新的 sorted 数组,类似于 sort() 但不修改原数组可选的比较函数
toReversed()返回一个新的 reversed 数组,类似于 reverse() 但不修改原数组无参数
toSpliced(start, deleteCount, ...items)返回一个新的 splice 后的数组,类似于 splice() 但不修改原数组与 splice() 相同
with(index, value)返回一个在指定索引处更新了值的新数组要更新的索引和新值

用法及输出

// 原始数组
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);

2万能的 toSpliced

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]

3toSpliced 和 with 的区别

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

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

举例说明:假设原数组为 [1, 2, 3]:

const arr = [1, 2, 3];
const newArr = arr.toSpliced(1, 1, '替换'); 
// 结果:[1, '替换', 3]
// 过程:删除索引1的元素(2),插入'替换',数组长度不变(仍为3)
const arr = [1, 2, 3];
const newArr = arr.with(1, '替换'); 
// 结果:[1, '替换', 3]
// 过程:直接将索引1的元素(2)替换为'替换',数组长度不变
📌 这两种方式在这个例子中结果相同,但本质不同:toSpliced 是"删除 + 插入"的组合操作(先删后加);with 只是单纯的"替换"操作(直接覆盖)。
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 完全无法实现,因为它:只能修改单个索引的元素;不能删除或插入元素(无法改变数组长度);一次只能处理一个元素。

4React 更新变量时为何需要拷贝

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

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

原始写法

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);
};

新方法写法及原因

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());
};

错误示范

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 已经被修改(违反不可变性);组件可能不会重新渲染(因为引用没变)。

5结尾

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