Back

非变异数组:编写更安全的 JavaScript 代码

非变异数组:编写更安全的 JavaScript 代码

当你在 JavaScript 中修改数组时,可能会意外地改变代码其他部分所依赖的数据。这会产生难以追踪的错误。解决方案是什么?使用返回新数组而不是更改原始数组的非变异数组方法。

本文涵盖了 JavaScript 中的基本非变异数组方法,解释了它们对编写更安全代码的重要性,以及如何在项目中有效使用它们。

关键要点

  • 非变异数组方法返回新数组而不改变原始数据
  • 使用不可变操作可以防止意外的副作用,使代码更可预测
  • map()filter()reduce() 等方法是变异操作的更安全替代方案
  • 扩展运算符为常见数组操作提供了简洁的语法

为什么不可变性在 JavaScript 中很重要

变异数组可能导致应用程序中的意外行为。当你将数组传递给函数或在组件之间共享时,在一个地方的修改会影响对该数组的所有引用。

const originalTasks = ['Write code', 'Review PR', 'Deploy'];
const completedTasks = originalTasks;
completedTasks.push('Write tests');

console.log(originalTasks); // ['Write code', 'Review PR', 'Deploy', 'Write tests']
// 原始数组被改变了!

这在 React 应用程序中尤其成问题,状态变异会阻止组件重新渲染,或者在 Redux 中状态必须保持不可变。

变异与非变异方法:关键区别

变异方法(避免使用这些)

  • push()pop()shift()unshift() - 添加或删除元素
  • sort() - 就地排序数组
  • reverse() - 反转数组顺序
  • splice() - 在任何位置添加/删除元素
  • fill() - 用值填充数组

非变异方法(使用这些)

  • map() - 转换每个元素
  • filter() - 保留符合条件的元素
  • reduce() - 将元素组合成单个值
  • slice() - 提取数组的一部分
  • concat() - 组合数组

基本的非变异数组方法

map():无变异转换

不使用修改数组的 for 循环,map() 创建一个包含转换值的新数组:

const prices = [10, 20, 30];
const discountedPrices = prices.map(price => price * 0.8);

console.log(prices);          // [10, 20, 30] - 未改变
console.log(discountedPrices); // [8, 16, 24]

filter():安全的数组过滤

在不触及原始数组的情况下删除元素:

const users = [
  { name: 'Alice', active: true },
  { name: 'Bob', active: false },
  { name: 'Charlie', active: true }
];

const activeUsers = users.filter(user => user.active);
console.log(users.length);       // 3 - 原始数组未改变
console.log(activeUsers.length); // 2

reduce():无副作用的组合

在不使用外部变量的情况下从数组计算值:

const orders = [
  { product: 'Laptop', price: 1200 },
  { product: 'Mouse', price: 25 }
];

const total = orders.reduce((sum, order) => sum + order.price, 0);
// 返回 1225 而不修改 orders

slice():提取数组部分

在不使用 splice() 的情况下获取数组的子集:

const tasks = ['Task 1', 'Task 2', 'Task 3', 'Task 4'];
const firstTwo = tasks.slice(0, 2);
const lastTwo = tasks.slice(-2);

console.log(firstTwo); // ['Task 1', 'Task 2']
console.log(lastTwo);  // ['Task 3', 'Task 4']
console.log(tasks);    // 原始数组未改变

concat():安全地组合数组

在不使用 push() 的情况下合并数组:

const completed = ['Task 1', 'Task 2'];
const pending = ['Task 3', 'Task 4'];
const allTasks = completed.concat(pending);

// 或使用扩展运算符
const allTasksSpread = [...completed, ...pending];

JavaScript 非变异数组最佳实践

1. 替换变异操作

// ❌ 避免:使用 push 进行变异
const items = [1, 2, 3];
items.push(4);

// ✅ 更好:创建新数组
const newItems = [...items, 4];

2. 链式调用方法进行复杂操作

const products = [
  { name: 'Laptop', price: 1200, inStock: true },
  { name: 'Phone', price: 800, inStock: false },
  { name: 'Tablet', price: 600, inStock: true }
];

const affordableInStock = products
  .filter(p => p.inStock)
  .filter(p => p.price < 1000)
  .map(p => p.name);
// 返回 ['Tablet'] 而不修改 products

3. 使用扩展运算符进行简单操作

// 删除指定索引的项目
const removeAt = (arr, index) => [
  ...arr.slice(0, index),
  ...arr.slice(index + 1)
];

// 更新指定索引的项目
const updateAt = (arr, index, value) => [
  ...arr.slice(0, index),
  value,
  ...arr.slice(index + 1)
];

React 中更安全的状态管理

非变异方法对于 React 状态更新至关重要:

function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Learn React', done: false }
  ]);

  const toggleTodo = (id) => {
    // ✅ 创建包含更新对象的新数组
    setTodos(todos.map(todo =>
      todo.id === id 
        ? { ...todo, done: !todo.done }
        : todo
    ));
  };

  const removeTodo = (id) => {
    // ✅ 过滤掉 todo 而不进行变异
    setTodos(todos.filter(todo => todo.id !== id));
  };
}

性能考虑

虽然非变异方法会创建新数组,但现代 JavaScript 引擎很好地优化了这些操作。可预测、无错误代码的好处通常超过了轻微的性能差异。对于具有大型数据集的性能关键代码,考虑使用专门的库,如 Immutable.jsImmer

结论

非变异数组方法使你的 JavaScript 代码更可预测且更易于调试。通过使用 map()filter()reduce()slice()concat() 而不是它们的变异对应方法,你可以避免导致错误的副作用。这种方法在 React 应用程序中和遵循函数式编程原则时特别有价值。今天就开始在你的代码中替换变异操作——你未来的自己会感谢你。

常见问题

可以,但在每个上下文中要保持一致。对于共享数据、状态管理和函数式编程使用非变异方法。对于不会在其他地方引用的本地临时数组,变异方法可以接受。

对于大多数应用程序,性能差异可以忽略不计。现代 JavaScript 引擎高效地优化这些操作。只有在分析确认瓶颈后,才考虑为极大数据集或性能关键循环使用替代方案。

使用扩展运算符或 slice 先创建副本,然后对副本进行排序。例如,const sorted = [...array].sort() 或 const sorted = array.slice().sort()。这样可以保持原始数组的顺序。

Slice 是非变异的,返回包含提取元素的新数组而不改变原始数组。Splice 是变异的,通过删除或替换元素直接修改原始数组,并返回被删除的元素。

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Check our GitHub repo and join the thousands of developers in our community.

OpenReplay