非变异数组:编写更安全的 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];
Discover how at OpenReplay.com.
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.js 或 Immer。
结论
非变异数组方法使你的 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.