非变异数组:编写更安全的 JavaScript 代码
介绍如何使用 map、filter、reduce、slice、concat 等非变异数组方法,避免 React 应用中的副作用与错误。
当你在 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 在数组操作中有什么区别?
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.
Star on GitHub12k