React(以及广义上的JavaScript)中的“浅层复制”(Shallow Copy)和“深层复制”(Deep Copy)。
理解这两个概念对于在React中正确地管理状态(State)至关重要,因为它们直接关系到React的更新机制和性能优化。
核心概念:基本类型 vs. 引用类型
在深入讲解复制之前,必须先理解JavaScript中的两种数据类型:
-
基本类型 (Primitive Types):
string,number,boolean,null,undefined,symbol,bigint。- 这些值直接存储在变量访问的位置(栈内存)。
- 当你把一个基本类型的值赋给另一个变量时,你是在复制这个值本身。
let a = 10; let b = a; // b 得到的是 10 这个值的副本 b = 20; // 修改 b 不会影响 a console.log(a); // 输出 10 -
引用类型 (Reference Types):
Object,Array,Function。- 这些值(对象本身)存储在堆内存中,而变量访问的位置(栈内存)只存储一个指向该对象的内存地址(引用)。
- 当你把一个引用类型的值赋给另一个变量时,你只是在复制这个内存地址,而不是对象本身。两个变量指向的是同一个对象。
let objA = { name: 'Alice' }; let objB = objA; // objB 得到的是指向 { name: 'Alice' } 这个对象的内存地址的副本 objB.name = 'Bob'; // 通过 objB 修改对象 console.log(objA.name); // 输出 'Bob',因为 objA 和 objB 指向同一个对象
浅层复制和深层复制主要就是针对“引用类型”的复制问题。
一、 浅层复制 (Shallow Copy)
1. 什么是浅层复制?
浅层复制会创建一个新的对象或数组,但它只复制第一层的属性。
- 如果第一层的属性是基本类型,它会复制该值的副本。
- 如果第一层的属性是引用类型(比如一个嵌套的对象或数组),它只会复制该引用类型的内存地址。
通俗比喻:
想象你有一本地址簿(原始对象),里面记录了朋友们的姓名和电话号码(基本类型),还记录了另一个地址簿的存放位置(引用类型)。
- 浅层复制就像是你买了一本新的地址簿,然后把原地址簿里的姓名和电话号码抄了一遍。但是,对于“另一个地址簿的存放位置”,你只是把那个位置信息抄了过来,并没有去复印那个地址簿本身。
- 结果就是,你有了两本独立的顶级地址簿,但它们都指向同一个“另一个地址簿”。如果你修改了那个共享的地址簿,两本顶级地址簿看到的内容都会改变。
2. 如何在JavaScript/React中实现浅层复制?
在现代React中,最常用的方法是展开语法 (Spread Syntax ...) 和一些数组方法。
展开运算符:React中的展开运算符 - 编程技术 - CEPD@BBS 。
-
对于对象:
const originalObject = { name: 'Admin', level: 99, details: { role: 'Superuser', permissions: ['read', 'write', 'delete'] } }; // 使用展开语法进行浅层复制 const shallowCopy = { ...originalObject }; // 验证 console.log(shallowCopy === originalObject); // false,因为 shallowCopy 是一个新的对象 // 修改第一层的基本类型 shallowCopy.level = 100; console.log(originalObject.level); // 99 (原始对象未受影响) // 修改嵌套对象的属性 shallowCopy.details.role = 'Guest'; console.log(originalObject.details.role); // 'Guest' (原始对象被影响了!) // 因为 shallowCopy.details 和 originalObject.details 指向同一个对象 -
对于数组:
const originalArray = [1, 2, { id: 3 }]; // 使用展开语法 const shallowCopy1 = [...originalArray]; // 使用 slice() 方法 const shallowCopy2 = originalArray.slice(); // 使用 Array.from() const shallowCopy3 = Array.from(originalArray); // 修改嵌套对象的属性 shallowCopy1[2].id = 99; console.log(originalArray[2].id); // 99 (原始对象被影响了!)
3. 为什么在React中如此重要?
React的核心原则之一是状态不可变性 (Immutability)。当组件的状态(state)更新时,React会通过比较新旧状态的引用来决定是否需要重新渲染。
-
错误的做法 (直接修改状态):
const [user, setUser] = useState({ name: 'Alice', age: 25 }); function handleAgeIncrease() { // 错误!直接修改了原始状态对象 user.age = 26; setUser(user); // user 的内存地址没有改变,React 认为状态没有变化,不会重新渲染! } -
正确的做法 (使用浅层复制):
const [user, setUser] = useState({ name: 'Alice', age: 25 }); function handleAgeIncrease() { // 正确!创建了一个新的对象 const newUser = { ...user, age: 26 }; setUser(newUser); // newUser 是一个新对象,有新的内存地址,React 检测到引用变化,触发重新渲染 }
对于大部分React状态更新场景,浅层复制已经足够了,因为它能确保顶层状态对象的引用发生变化,从而触发React的更新。
二、 深层复制 (Deep Copy)
1. 什么是深层复制?
深层复制会创建一个完全独立的副本。它会递归地复制原始对象或数组中的所有层级,包括所有嵌套的对象和数组。
- 复制后的对象与原始对象没有任何共享的引用。修改新对象中的任何内容(无论多深),都不会影响到原始对象。
通俗比喻:
回到地址簿的例子。
- 深层复制就像是不仅买了一本新地址簿,抄写了姓名电话,而且当你遇到“另一个地址簿的存放位置”时,你会找到那个地址簿,把它也完完整整地复印一本,然后把这本全新复印本的位置记录在新地址簿里。这个过程会一直持续下去,直到所有嵌套的东西都被复制为全新的副本。
- 结果是,你得到了两套完全独立的地址簿系统,互不干扰。
2. 如何在JavaScript/React中实现深层复制?
实现深层复制比浅层复制要复杂。
-
方法一:
JSON.parse(JSON.stringify(obj))(简单但有缺陷)这是最简单快捷的方法,但有几个重要的缺点:
- 会丢失
undefined、function、symbol类型的值。 - 会把
Date对象转换为字符串。 - 不能处理循环引用(对象属性直接或间接引用自身),会报错。
const originalObject = { name: 'Admin', joined: new Date(), details: { role: 'Superuser' }, logout: () => console.log('logout') }; const deepCopy = JSON.parse(JSON.stringify(originalObject)); // 修改嵌套对象的属性 deepCopy.details.role = 'Guest'; console.log(originalObject.details.role); // 'Superuser' (原始对象未受影响,成功!) // 检查缺陷 console.log(originalObject.joined); // Date 对象 console.log(deepCopy.joined); // 变成了一个字符串 console.log(deepCopy.logout); // undefined (函数丢失了) - 会丢失
-
方法二:使用第三方库 (推荐)
在生产环境中,最可靠的方法是使用成熟的库,例如 Lodash 的
cloneDeep方法。import { cloneDeep } from 'lodash'; const originalObject = { /* ... */ }; const deepCopy = cloneDeep(originalObject); // 完美处理各种情况 -
方法三:手动实现递归函数
你可以自己编写一个递归函数来遍历对象的所有属性,并为每个属性创建副本。这很复杂,容易出错,通常不推荐自己写。
3. 什么时候在React中使用深层复制?
当你有一个深度嵌套的状态,并且你需要修改深层的某个属性时,就需要深层复制。
场景示例:
假设你有一个复杂的表单状态,结构如下:
const [formState, setFormState] = useState({
user: {
name: 'Alice',
address: {
city: 'New York',
zip: '10001'
}
},
items: [
{ id: 1, name: 'Laptop' }
]
});
如果你想修改 city,只用浅层复制是不够的:
function updateCity(newCity) {
// 仅浅层复制第一层
const newFormState = { ...formState };
// newFormState.user 和 formState.user 仍然是同一个对象!
newFormState.user.address.city = newCity; // 这样仍然修改了原始状态!这是错误的!
setFormState(newFormState);
}
正确的做法(不使用深拷贝库,手动逐层复制):
function updateCity(newCity) {
setFormState(prevState => ({
...prevState, // 1. 复制顶层
user: {
...prevState.user, // 2. 复制 user 层
address: {
...prevState.user.address, // 3. 复制 address 层
city: newCity // 4. 更新 city
}
}
}));
}
这种手动逐层展开的方式是React中处理嵌套状态更新的常见模式,它避免了引入整个深拷贝库的开销。
如果状态结构非常复杂,或者更新逻辑繁琐,使用深拷贝库(如Lodash)或状态管理库(如Immer)会更方便。
总结与对比
| 特性 | 浅层复制 (Shallow Copy) | 深层复制 (Deep Copy) |
|---|---|---|
| 定义 | 只复制对象/数组的第一层。嵌套的引用类型只复制其内存地址。 | 递归复制所有层级,创建一个完全独立的副本。 |
| 性能 | 速度快,开销小。 | 速度慢,开销大,因为它需要遍历整个数据结构。 |
| 实现方式 | ... (展开语法), Object.assign(), Array.slice() |
JSON.parse(JSON.stringify()) (有缺陷), lodash.cloneDeep() (推荐) |
| 共享引用 | 内部的引用类型(嵌套对象/数组)是共享的。 | 没有任何共享引用。 |
| React使用场景 | 绝大多数情况。用于更新状态对象或数组的顶层属性,以触发重新渲染。 | 少数情况。当状态结构非常深,且需要修改深层嵌套的属性时使用。 |
| 黄金法则 | 默认使用浅层复制。当且仅当浅层复制导致你意外地修改了原始状态时,才考虑深层复制。 | / |