1 React 状态更新的 "不可变性" 原则

要理解 React 中操作数组 state 的这张参考表,需从 React 状态更新的 "不可变性" 原则 入手:React 依赖状态的 "引用变化" 来识别更新,若直接修改原始数组(可变操作),React 可能无法检测到变化,导致组件不重新渲染。以下对表格内容逐一详解:
1. 添加元素
- 避免使用 :
push(在数组末尾添加元素)、unshift(在数组开头添加元素)。原因:这两个方法会直接修改原始数组,破坏状态的不可变性。 - 推荐使用 :
concat或 展开语法 ([...arr]) 。concat:它会返回一个新数组 ,包含原始数组和新增元素。例如:const newArr = arr.concat(newItem)。- 展开语法:通过
[...原数组, 新元素]生成新数组。例如:const newArr = [...arr, newItem](末尾添加),const newArr = [newItem, ...arr](开头添加)。
2. 删除元素
- 避免使用 :
pop(删除数组最后一个元素)、shift(删除数组第一个元素)、splice(通过索引删除元素)。原因:这些方法会直接修改原始数组。 - 推荐使用 :
filter或slice。filter:通过回调函数筛选元素,返回新数组 (不包含被删除的元素)。例如,删除值为target的元素:const newArr = arr.filter(item => item !== target)。slice:用于截取数组片段,返回新数组 。例如,删除第一个元素:const newArr = arr.slice(1);删除最后一个元素:const newArr = arr.slice(0, -1)。
3. 替换元素
-
避免使用 :
splice(通过索引替换元素)、直接对数组索引赋值(如arr[i] = newVal)。原因:这两种方式都会直接修改原始数组。 -
推荐使用 :
map。map会遍历数组,对每个元素执行回调函数并返回新数组 。例如,将索引为i的元素替换为newVal:javascriptconst newArr = arr.map((item, index) => { if (index === i) return newVal; return item; });
4. 排序
-
避免使用 :
reverse(反转数组)、sort(排序数组)。原因:这两个方法会直接修改原始数组。 -
推荐使用 :先复制数组,再执行排序操作 。先通过展开语法或
slice复制数组,再调用排序方法(此时修改的是复制后的新数组,不影响原始状态)。例如:javascript// 排序 const newArr = [...arr].sort((a, b) => a - b); // 反转 const newArr = [...arr].reverse();
补充:Immer 库的作用
如果觉得上述 "不可变操作" 过于繁琐,可以使用 Immer 库 。它允许你以 "可变" 的写法操作状态,底层会自动生成不可变的新状态。这样你就可以直接使用 push、splice 等方法,而不用担心破坏 React 状态的不可变性。
2 slice 和 splice 方法

1. 方法作用差异
| 方法 | 作用 | 是否修改原始数组 |
|---|---|---|
| slice | 拷贝数组或数组的一部分 | 否(返回新数组) |
| splice | 插入或删除元素 | 是(直接修改) |
2. React 中的使用建议
在 React 中,由于状态更新需要遵循不可变性原则 (即不能直接修改原始状态,需返回新状态让 React 识别更新),因此更推荐使用 slice,而应避免使用会直接修改原始数组的 splice。
3 JavaScript 的引用类型特性 和状态管理的不可变性原则

1. 数组的 "浅拷贝" 问题
代码中 const myNextList = [...myList]; 是对 myList 数组的浅拷贝。
- 浅拷贝只会复制数组的 "表层结构"(即数组的长度、元素的引用),但数组内部的对象元素(如
artwork)并不会被重新创建,而是仍然指向原数组中对象的内存地址。 - 换句话说,
myNextList和myList虽然是两个不同的数组,但它们内部的artwork对象是 "同一个"(共享同一块内存)。
2. 直接修改对象带来的副作用
当执行 artwork.seen = nextSeen; 时,因为 artwork 是原数组 myList 中对象的引用,所以这个修改会直接影响原数组中的对象。
- 如果还有其他地方(比如
yourList)也引用了这个artwork对象,那么这些地方的状态也会被意外修改,从而引发难以排查的 bug。
3. 如何解决?(遵循 "不可变" 原则)
要避免直接修改原有对象,需要创建新的对象副本 来承载修改。可以用 map 方法实现:
javascript
const myNextList = myList.map(item => {
if (item.id === artworkId) {
// 对匹配的对象,返回一个新的对象(包含修改后的属性)
return { ...item, seen: nextSeen };
}
// 不匹配的对象,返回原对象(保持不变)
return item;
});
setMyList(myNextList);
这样做的核心是:修改时创建新对象,而非直接修改原有对象,从而保证原数组的状态不会被意外污染,也符合 React 等框架对状态 "不可变" 的设计预期。
4 状态管理中数组操作的 "不可变性" 原则

这部分内容是关于状态管理中数组操作的 "不可变性" 原则(常见于 React 等前端框架的状态管理场景),以下是逐条详解:
1. "你可以把数组放入 state 中,但你不应该直接修改它。"
- 框架(如 React)的状态更新机制依赖 "引用变化" 来识别更新。如果直接修改 state 中的数组(比如
state.arr[0] = 1),数组的引用地址没有变化,框架可能无法识别到状态更新,导致界面不渲染;同时直接修改会破坏 "不可变性",引发难以排查的副作用(比如多个地方共享状态时的意外污染)。
2. "不要直接修改数组,而是创建它的一份新的拷贝,然后使用新的数组来更新它的状态。"
- 为了保证状态的 "不可变性",任何对数组的修改都需要创建新的数组实例 。例如原数组是
[1,2,3],要修改第一个元素,需创建[4,2,3]这个新数组,再用它更新状态。这样框架能通过引用变化识别更新,也能避免副作用。
3. "你可以使用 [...arr, newItem] 这样的数组展开语法来向数组中添加元素。"
- 这是创建 "新数组" 的常用技巧。例如原数组
arr = [1,2],执行const newArr = [...arr, 3]后,newArr是[1,2,3],且newArr是全新的数组引用 ,原arr不会被修改。这种方式既实现了 "添加元素" 的逻辑,又保证了不可变性。
4. "你可以使用 filter() 和 map() 来创建一个经过过滤或者变换的数组。"
filter():用于 "过滤元素",会返回一个新数组 。例如const newArr = arr.filter(item => item > 2),原arr不变,newArr是过滤后的新数组。map():用于 "变换元素",会返回一个新数组 。例如const newArr = arr.map(item => item * 2),原arr不变,newArr是元素变换后的新数组。- 这两个方法天然符合 "不可变性",因为它们都不会修改原数组,而是返回新数组。
5. "你可以使用 Immer 来保持代码简洁。"
-
Immer 是一个 JavaScript 库,它的核心是 "用 mutable 的写法实现 immutable 的效果 "。在处理复杂状态(比如嵌套数组、对象)时,直接写 "修改式" 代码会很繁琐,而 Immer 可以让你以更简洁的方式创建新状态。例如:
javascriptimport { produce } from 'immer'; const newState = produce(originalState, draft => { draft.arr[0] = 1; // 看似直接修改,但 Immer 会自动生成新的不可变状态 });它隐藏了 "创建新拷贝" 的细节,让代码更简洁易读。
总结 :这些规则的核心是保证状态的 "不可变性"------任何状态修改都要通过 "创建新引用" 来实现,而非直接修改原状态。这既是前端框架状态管理的最佳实践,也能从根源上避免因共享引用导致的副作用 bug。
5 JavaScript 分号使用与数组更新注意事项
一、分号的正确使用规范
在 JavaScript 中,分号用于标记语句的结束,虽然存在 "自动分号插入(ASI)" 机制,但并非所有场景都能可靠生效,因此建议主动添加分号以避免语法错误。
1. 必须添加分号的场景
-
变量声明语句结尾
javascriptconst initialProducts = [/* ... */]; // 数组赋值后需加分号 const [products, setProducts] = useState(initialProducts); // 解构赋值后需加分号 -
函数体内的执行语句结尾
javascriptfunction handleIncreaseClick(productId) { setProducts(products.map(product => { if (product.id === productId) { return { ...product, count: product.count + 1 }; // return语句的对象后加分号 } return product; // return语句的返回值后加分号 })); // map函数调用结束后加分号 } -
对象 / 数组字面量作为独立语句时
javascript// 错误示例(可能被解析为其他语法) const obj = { x: 1 } [1, 2, 3].forEach(...) // 正确示例 const obj = { x: 1 }; [1, 2, 3].forEach(...);
2. 分号的作用
- 明确语句边界,避免 JavaScript 引擎误解析(例如将换行后的代码拼接为同一语句)。
- 提高代码可读性,让开发者清晰区分不同语句的范围。
二、数组更新中的核心注意事项(以购物车为例)
使用 map 方法更新数组时,需注意以下两点:
1. 正确引用当前元素的属性
在 map 循环中,需通过循环变量(如 product)访问当前元素的属性,避免因变量未定义导致错误。
javascript
// 错误写法(未指定具体元素的count)
return { ...product, count: count + 1 };
// 正确写法(通过product访问当前元素的count)
return { ...product, count: product.count + 1 };
2. 确保所有分支都有返回值
map 方法需要为数组中的每个元素返回一个值,否则会生成 undefined 元素,导致数组结构异常。
javascript
// 错误写法(未匹配时无返回值)
products.map(product => {
if (product.id === productId) {
return { ...product, count: product.count + 1 };
}
// 缺少else分支的返回值
});
// 正确写法(所有情况均返回值)
products.map(product => {
if (product.id === productId) {
return { ...product, count: product.count + 1 };
}
return product; // 未匹配时返回原元素
});
三、总结
- 分号使用:主动为语句添加分号,尤其在变量声明、函数调用、对象 / 数组字面量结尾,避免依赖自动分号插入。
- 数组更新 :使用
map时,需通过循环变量访问属性,并确保每个分支都有返回值,保证数组结构完整。
遵循以上规范可减少语法错误,提高代码的可靠性和可维护性。
6 JS代码修改对比(语法错误)
原错误代码(存在问题的版本)
javascript
import { useState } from 'react';
const initialProducts = [{
id: 0,
name: 'Baklava',
count: 1,
}, {
id: 1,
name: 'Cheese',
count: 5,
}, {
id: 2,
name: 'Spaghetti',
count: 2,
}];
export default function ShoppingCart() {
const [
products,
setProducts
] = useState(initialProducts)
function handleIncreaseClick(productId) {
// 问题1:循环变量与数组重名(products)
// 问题2:对象语法错误(多余括号、分号、未正确引用属性)
// 问题3:缺少闭合括号
setProducts(products.map(products => {
if(product.id !== productId){
return product;
}
else{
return{(
...products,
count : count + 1 ;
)}
}
});
}
return (
<ul>
{products.map(product => (
<li key={product.id}>
{product.name}
{' '}
(<b>{product.count}</b>)
<button onClick={() => {
handleIncreaseClick(product.id);
}}>
+
</button>
</li>
))}
</ul>
);
}
修改后代码(正确版本)
javascript
import { useState } from 'react';
const initialProducts = [{
id: 0,
name: 'Baklava',
count: 1,
}, {
id: 1,
name: 'Cheese',
count: 5,
}, {
id: 2,
name: 'Spaghetti',
count: 2,
}];
export default function ShoppingCart() {
const [
products,
setProducts
] = useState(initialProducts)
function handleIncreaseClick(productId) {
// 修复1:循环变量用单数(product),与数组(products)区分
// 修复2:对象语法正确(去掉多余括号和分号,用product.count)
// 修复3:补全闭合括号
setProducts(products.map(product => {
if (product.id === productId) {
return {
...product,
count: product.count + 1
};
} else {
return product;
}
}));
}
return (
<ul>
{products.map(product => (
<li key={product.id}>
{product.name}
{' '}
(<b>{product.count}</b>)
<button onClick={() => {
handleIncreaseClick(product.id);
}}>
+
</button>
</li>
))}
</ul>
);
}
关键修改点说明
-
循环变量命名 原代码用
products作为循环变量(与数组重名),修改后用product(单数),明确区分 "数组整体" 和 "单个元素",避免product.id被误解析。 -
对象语法修正 原代码中
return{( ...products, count : count + 1 ; )}存在多处错误:- 去掉多余的括号嵌套(
{( )}→{ }); - 移除对象内部的分号(
count: count + 1;→count: product.count + 1); - 用
product.count正确引用当前元素的数量属性(原代码漏写product.)。
- 去掉多余的括号嵌套(
-
语法完整性 原代码
setProducts(...)末尾缺少闭合括号),修改后补全,确保函数调用语法正确。
7 filter删除数组
filter 就像个 "过滤器",专门从数组里挑出符合条件的元素,组成一个新数组 ------ 简单说就是 "留下想要的,扔掉不想要的"。
基本用法:
javascript
const 新数组 = 原数组.filter(元素 => 条件);
- 遍历原数组的每个元素,把符合 "条件" 的元素放进新数组;
- 不修改原数组,而是返回一个全新的数组(这点对 React state 很重要!)。
拿你的删除功能举例:
你要删除 id = todoId 的任务,意思就是 "留下所有 id 不等于 todoId 的任务":
javascript
function handleDeleteTodo(todoId) {
// 原数组是 todos,过滤出所有 id 不等于 todoId 的元素
const newTodos = todos.filter(todo => todo.id !== todoId);
// 用新数组更新 state
setTodos(newTodos);
}
todo => todo.id !== todoId就是筛选条件:每个todo都要检查,如果它的id不等于要删除的todoId,就留下它;- 最后
newTodos里就是所有没被删除的任务,用setTodos更新后,页面就会同步显示删除后的列表。
再举个通俗例子:
假设数组是 [1,2,3,4,5],想筛选出所有偶数:
javascript
const evenNumbers = [1,2,3,4,5].filter(num => num % 2 === 0);
// evenNumbers 结果是 [2,4]
和删除逻辑一样:符合 "是偶数" 条件的留下,不符合的扔掉。
记住核心:
filter 不会动原数组,只会返回一个 "符合你要求" 的新数组 ------ 这正好符合 React 中 "不能直接修改 state,要返回新值" 的规矩,所以删除、筛选这类操作特别适合用它。
下次再用就想:"我要留下什么样的元素?" 把这个条件写成箭头函数,传给 filter 就行~