React 17
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:const newArr = arr.map((item, index) => {if (index === i) return newVal;return item; });
4. 排序
- 避免使用:
reverse(反转数组)、sort(排序数组)。原因:这两个方法会直接修改原始数组。 - 推荐使用:先复制数组,再执行排序操作。先通过展开语法或
slice复制数组,再调用排序方法(此时修改的是复制后的新数组,不影响原始状态)。例如:// 排序 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 方法实现:
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 可以让你以更简洁的方式创建新状态。例如:
它隐藏了 “创建新拷贝” 的细节,让代码更简洁易读。import { produce } from 'immer';const newState = produce(originalState, draft => {draft.arr[0] = 1; // 看似直接修改,但 Immer 会自动生成新的不可变状态 });
总结:这些规则的核心是保证状态的 “不可变性”——任何状态修改都要通过 “创建新引用” 来实现,而非直接修改原状态。这既是前端框架状态管理的最佳实践,也能从根源上避免因共享引用导致的副作用 bug。
5 JavaScript 分号使用与数组更新注意事项
一、分号的正确使用规范
在 JavaScript 中,分号用于标记语句的结束,虽然存在 “自动分号插入(ASI)” 机制,但并非所有场景都能可靠生效,因此建议主动添加分号以避免语法错误。
1. 必须添加分号的场景
变量声明语句结尾
const initialProducts = [/* ... */]; // 数组赋值后需加分号 const [products, setProducts] = useState(initialProducts); // 解构赋值后需加分号函数体内的执行语句结尾
function handleIncreaseClick(productId) {setProducts(products.map(product => {if (product.id === productId) {return { ...product, count: product.count + 1 }; // return语句的对象后加分号}return product; // return语句的返回值后加分号})); // map函数调用结束后加分号 }对象 / 数组字面量作为独立语句时
// 错误示例(可能被解析为其他语法) const obj = { x: 1 } [1, 2, 3].forEach(...)// 正确示例 const obj = { x: 1 }; [1, 2, 3].forEach(...);
2. 分号的作用
- 明确语句边界,避免 JavaScript 引擎误解析(例如将换行后的代码拼接为同一语句)。
- 提高代码可读性,让开发者清晰区分不同语句的范围。
二、数组更新中的核心注意事项(以购物车为例)
使用 map 方法更新数组时,需注意以下两点:
1. 正确引用当前元素的属性
在 map 循环中,需通过循环变量(如 product)访问当前元素的属性,避免因变量未定义导致错误。
// 错误写法(未指定具体元素的count)
return { ...product, count: count + 1 };// 正确写法(通过product访问当前元素的count)
return { ...product, count: product.count + 1 };
2. 确保所有分支都有返回值
map 方法需要为数组中的每个元素返回一个值,否则会生成 undefined 元素,导致数组结构异常。
// 错误写法(未匹配时无返回值)
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代码修改对比(语法错误)
原错误代码(存在问题的版本)
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>);
}
修改后代码(正确版本)
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 就像个 “过滤器”,专门从数组里挑出符合条件的元素,组成一个新数组 —— 简单说就是 “留下想要的,扔掉不想要的”。
基本用法:
const 新数组 = 原数组.filter(元素 => 条件);
- 遍历原数组的每个元素,把符合 “条件” 的元素放进新数组;
- 不修改原数组,而是返回一个全新的数组(这点对 React state 很重要!)。
拿你的删除功能举例:
你要删除 id = todoId 的任务,意思就是 “留下所有 id 不等于 todoId 的任务”:
function handleDeleteTodo(todoId) {// 原数组是 todos,过滤出所有 id 不等于 todoId 的元素const newTodos = todos.filter(todo => todo.id !== todoId);// 用新数组更新 statesetTodos(newTodos);
}
todo => todo.id !== todoId就是筛选条件:每个todo都要检查,如果它的id不等于要删除的todoId,就留下它;- 最后
newTodos里就是所有没被删除的任务,用setTodos更新后,页面就会同步显示删除后的列表。
再举个通俗例子:
假设数组是 [1,2,3,4,5],想筛选出所有偶数:
const evenNumbers = [1,2,3,4,5].filter(num => num % 2 === 0);
// evenNumbers 结果是 [2,4]
和删除逻辑一样:符合 “是偶数” 条件的留下,不符合的扔掉。
记住核心:
filter 不会动原数组,只会返回一个 “符合你要求” 的新数组 —— 这正好符合 React 中 “不能直接修改 state,要返回新值” 的规矩,所以删除、筛选这类操作特别适合用它。
下次再用就想:“我要留下什么样的元素?” 把这个条件写成箭头函数,传给 filter 就行~
