JackAtlas

Command Palette

Search for a command to run...

[译] 终于,JavaScript 有了安全的数组方法
7 months ago
前端开发

[译] 终于,JavaScript 有了安全的数组方法

本文由小智根据《Finally, safe array methods in JavaScript》所译。译文带有我自己的理解和思想,如需转载请注明相关信息:

原文地址:Finally, safe array methods in JavaScript
——译者:小智

不难理解为什么许多开发者在使用 .sort().reverse() 或者 .splice() 这些 JavaScript 方法前会犹豫:这些方法修改了原数组。这就会导致一些难以察觉、难以追踪的 bug,特别是在一些共享状态和响应式的程序中。好消息是在过去的几年里我们获得了一些新的数组方法,不会影响到原数组,使得数组操作更加安全和干净。

  • toSorted()
  • toReversed()
  • toSpliced()

这些方法返回数组的拷贝而不是在原数组上做改动。一点点语法变动就能带来不小的影响,尤其是对依赖不可变性(immutability) 的 React 开发者来说。

就地修改型数组方法的问题

在 JavaScript 中,像 .sort().reverse().splice() 这样的传统方法在调用的时候会修改原数组:

const numbers = [3, 1, 2];
numbers.sort(); // Mutates the array
console.log(numbers); // [1, 2, 3]

在像 React 这样的框架中,这会在更新状态的时候导致不可预料的行为,因为直接修改数组并不会触发重新渲染。

新旧对比

操作修改型方法非修改型方法
排序arr.sort()arr.toSorted()
倒序arr.reversed()arr.toReversed()
替换arr.splice()arr.toSpliced()

这些新方法的行为和对应的老方法相似,只是返回了新的数组,而不是在原数组上修改。

注意:这些都是“浅拷贝”,如果你的数组含有对象,这些对象还是维持原来的引用。

解决方案:安全的、非修改型方法

ES2023 引入了这些非修改型的数组方法:

toSorted()

创建一个排序的拷贝,而不修改原来的数组。

const numbers = [3, 1, 2];
const sorted = numbers.toSorted();

console.log(sorted); // [1, 2, 3]
console.log(numbers); // [3, 1, 2] ✅

// ‼️对比:.sort() 会修改原数组
numbers.sort();
console.log(numbers); // [1, 2, 3] ❌

你也可以传入一个用于对比的方法,就像 .sort() 那样:

const users = [
  { name: 'Kristen', age: 36 },
  { name: 'David', age: 34 }
]

const byAge = users.toSorted((a, b) => a.age - b.age);

console.log(byAge); // [{ name: 'David', age: 34 }, { name: 'Kristen', age: 36 }]
console.log(users); // [{ name: 'Kristen', age: 36 }, { name: 'David', age: 34 }]

toReversed()

返回数组的倒序拷贝

const names = ['Kristen', 'David', 'Ben']
const reversed = names.toReversed();

console.log(reversed); // ['Ben', 'David', 'Kristen']
console.log(names); // ['Kristen', 'David', 'Ben'] ✅

// ‼️对比:.reverse() 会修改原数组
names.reverse();
console.log(names); // ['Ben', 'David', 'Kristen'] ❌

当你想展示一个倒序的列表而不修改原有数据时这就很完美。

toSpliced()

.splice() 方法的一个安全替代。返回包含新增/删除元素的新数组,而不染指旧数据

const items = ['a', 'b', 'c', 'd'];

// 移除下标 1 的元素
const withoutB = items.toSpliced(1, 1);

console.log(withoutB); // ['a', 'c', 'd']
console.log(items);    // ['a', 'b', 'c', 'd'] ✅

// 添加新元素到下标 2
const withX = items.toSpliced(2, 0, 'x');
console.log(withX); // ['a', 'b', 'x', 'c', 'd']

// ‼️对比:`.splice()` 会修改原数组
const items2 = ['a', 'b', 'c', 'd'];
const removed = items2.splice(1, 1);
console.log(removed);  // ['b']
console.log(items2);   // ['a', 'c', 'd'] ❌

提醒:.splice() 返回被移除的元素,而 .toSpliced() 返回更新后的数据。

为什么在 React 里这很重要

在 React 里,不可变数据是触发组件更新和保持数据可预测性的关键。

// ❌ 直接修改数据
state.items.sort(); // 不重新渲染

// ✅ 使用 toSorted
const sortedItems = state.items.toSorted();
setState({ items: sortedItems }); // 触发重新渲染

这些方法有助于你以不可变的方式对待数组,而无需使用 structuredClone() 或其他的深拷贝手段。

真实案例:在 React 中给任务排序

这里展示了如何在组件中使用 toSorted()toReversed() 方法来安全地展示动态列表:

function TaskList({ tasks }) {
  // 优先展示更临近的任务
  const recentFirst = tasks?.toReversed() ?? [];

  return (
    <ul>
      {recentFirst.map((task) => (
        <li key={task.id}>{task.title}</li>
      ))}
    </ul>
  );
}

这避免了在 tasks 是 props 或来源于 state 的情况下,对其修改而产生的 bug。而可选链修饰符(?.)和空值合并运算符(??)也能在 task 是 undefined 的时候避免产生错误。

小小的语法改动,大大的胜利

这些方法并不要求新的心智模型,它们仅仅是你已经在使用的老方法的升级版。如果你在现代环境(抑或使用 Babel 或 SWC),不妨试着使用它们。

浏览器支持

toSorted()toReversed()toSpliced() 在所有现代环境中都被支持(Chrome/Edge 110+,Safari 16+,Firefox 115+,Node.js 20+)。在老环境中,你可以考虑使用 core-js 作为补丁。