toSorted / toReversed / toSpliced / with:免拷贝更新 state
我们在写 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) 等。
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);
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]
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)替换为'替换',数组长度不变
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)
在 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 会认为状态没变化,不重新渲染
};