前言:为何需要深浅拷贝?
在 JavaScript 开发中,我们经常需要复制对象或数组,但直接赋值(如 let arr2 = arr1)会导致两个变量指向同一个内存地址,也就是当你修改其中一个时,另一个也会被改变,在很多场景下这是不可接受的。比如:
- 表单数据的深拷贝(避免修改原始数据)。
- 配置对象的合并(保留默认配置)。
- 状态管理(React 中避免直接修改 state)。
于是,深浅拷贝就成为了解决这一问题的关键工具。
一、浅拷贝:表面复制,深层共享
1. 浅拷贝的核心原理
浅拷贝的定义是仅复制对象的第一层属性,具体表现为:
- 基本类型(如数字、字符串、布尔值) :直接复制值到新对象,修改新对象的属性不会影响原对象。
- 引用类型(如对象、数组) :复制的是引用地址(内存地址),新对象和原对象共享同一块内存空间。因此,修改嵌套对象的属性会影响原对象。
2. 常见实现方式及特性
2.1 展开运算符(...)
const arr1 = [1, 2, 3];
const arr2 = [...arr1]; // 浅拷贝
arr2[1] = 'b'; // 修改第一层属性
console.log(arr1); // [1, 2, 3](原数组未受影响)
console.log(arr2); // [1, "b", 3](新数组已修改)
分析:
- 第一层属性独立:
arr2是arr1的浅拷贝,修改第一层属性(如arr2[1])不会影响arr1。 - 嵌套结构共享:如果
arr1包含嵌套数组或对象,修改嵌套结构会影响原数组(如arr1[0][0] = 100会同步到arr2[0][0])。
2.2 Object.assign()
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = Object.assign({}, obj1); // 浅拷贝
obj2.b.c = 3; // 修改嵌套属性
console.log(obj1.b.c); // 3(原对象被修改!)
分析:
- 顶层属性独立:
obj2.a的修改(如obj2.a = 10)不会影响obj1.a。 - 嵌套属性共享:
obj2.b是对obj1.b的引用,修改obj2.b.c会直接修改obj1.b.c。 - 适用场景:适合复制扁平化对象,但需注意嵌套结构的引用问题。
2.3 slice() / concat()
const arr1 = [[1, 2], [3, 4]];
const arr2 = arr1.slice(); // 浅拷贝
arr2[1][0] = 99; // 修改嵌套数组
console.log(arr1); // [[1, 2], [99, 4]](原数组被修改!)
分析:
- 数组结构独立:
arr2是arr1的新数组,但嵌套数组的引用被共享。 - 嵌套修改同步:修改
arr2中的嵌套数组元素(如arr2[1][0])会同步到arr1。 - 适用场景:适合简单数组的浅拷贝,但不适用于多维数组或对象嵌套。
3. 使用浅拷贝的参数前后变化
| 操作 | 原对象(arr1 / obj1) | 新对象(arr2 / obj2) |
|---|---|---|
修改第一层属性(如 a) | 无变化 | 新对象属性更新 |
修改嵌套属性(如 b.c) | 被同步修改 | 新对象属性更新 |
替换嵌套对象(如 b = {}) | 无变化 | 新对象属性独立 |
4. 适用场景与注意事项
适用场景
-
简单数据结构:
- 数据层级单一,无需递归复制(如
{ a: 1, b: 2 })。 - 示例:React 中的组件状态更新(通过浅拷贝避免直接修改原状态)。
- 数据层级单一,无需递归复制(如
-
性能敏感场景:
- 大型对象或数组的快速复制(如 Redux 的状态合并)。
- 浅拷贝速度比深拷贝快,内存占用更低。
注意事项
-
嵌套结构风险:
- 修改嵌套对象属性会导致原对象同步变化,需谨慎处理。
- 示例:
obj2.b.c = 3会修改obj1.b.c。
-
替代方案:
- 对于嵌套结构需完全独立,应使用深拷贝(如
JSON.parse(JSON.stringify())或第三方库lodash.cloneDeep)。
- 对于嵌套结构需完全独立,应使用深拷贝(如
-
循环引用问题:
- 浅拷贝无法处理对象间的循环引用(如
obj1.self = obj1),可能导致栈溢出。
- 浅拷贝无法处理对象间的循环引用(如
二、深拷贝:彻底复制,独立内存
1. 深拷贝的核心原理
深拷贝是指递归复制对象的所有层级属性,包括嵌套对象和数组。其特点如下:
- 完全独立:新对象与原对象在内存中互不干扰,修改新对象的任何属性都不会影响原对象。
- 递归处理:对嵌套结构(如对象、数组)进行逐层复制,直到所有层级均为基本类型或原始引用类型。
2. 常见实现方式及特性分析
2.1 JSON 序列化与反序列化
const obj1 = { a: 1, b: { c: 2 }, d: new Date(), e: undefined };
const obj2 = JSON.parse(JSON.stringify(obj1));
obj2.b.c = 3;
console.log(obj1.b.c); // 2(原对象未被修改)
功能特性:
-
优点:
- 代码简洁,适合纯数据对象的深拷贝。
- 性能较好(无需递归函数)。
-
缺点:
- 不支持非 JSON 兼容类型:函数、
undefined、Symbol等会被忽略。 - 特殊对象丢失:
Date对象会被转为字符串,RegExp、Map、Set等会被忽略。 - 循环引用报错:如
obj.self = obj会导致TypeError。
- 不支持非 JSON 兼容类型:函数、
2.2 递归拷贝(手动实现)
function deepClone(source) {
if (typeof source !== 'object' || source === null) return source;
const target = Array.isArray(source) ? [] : {};
for (const key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = deepClone(source[key]);
}
}
return target;
}
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = deepClone(obj1);
obj2.b.c = 3;
console.log(obj1.b.c); // 2(原对象未被修改)
功能特性:
-
优点:
- 支持嵌套对象和数组的递归复制。
- 可扩展性强(可添加对特殊对象的处理逻辑)。
-
缺点:
- 无法处理循环引用:如
obj.self = obj会导致栈溢出。 - 特殊对象需额外处理:如
Date、RegExp、Map、Set等需要单独判断。 - 内存开销较大:递归深度过大会导致性能下降。
- 无法处理循环引用:如
2.3 第三方库(如 Lodash 的 _.cloneDeep)
const _ = require('lodash');
const obj1 = { a: 1, b: { c: 2 }, d: new Date(), e: new Set([1, 2]) };
const obj2 = _.cloneDeep(obj1);
obj2.b.c = 3;
console.log(obj1.b.c); // 2(原对象未被修改)
功能特性:
-
优点:
- 功能全面:支持复杂数据类型(如
Date、RegExp、Map、Set)。 - 处理循环引用:通过记录已访问对象避免栈溢出。
- 兼容性好:经过广泛测试,适用于生产环境。
- 功能全面:支持复杂数据类型(如
-
缺点:
- 依赖外部库:需引入 Lodash,可能增加项目体积。
- 性能略低:相比手动实现的递归拷贝,复杂度稍高。
3. 使用深拷贝的参数前后变化
| 操作 | 原对象(obj1) | 新对象(obj2) |
|---|---|---|
修改第一层属性(如 a) | 无变化 | 新对象属性更新 |
修改嵌套属性(如 b.c) | 无变化 | 新对象属性更新 |
替换嵌套对象(如 b = {}) | 无变化 | 新对象属性独立 |
4. 适用场景与注意事项
适用场景
-
复杂嵌套结构:
- 需要完全隔离新旧对象(如配置对象、状态管理)。
- 示例:React 中的组件状态更新需避免引用共享。
-
特殊数据类型:
- 包含
Date、Map、Set等对象时,推荐使用第三方库。
- 包含
-
数据安全性要求高:
- 修改新对象不会影响原对象,适合敏感数据处理。
注意事项
-
避免循环引用:
- 递归实现需添加循环检测逻辑(如 WeakMap 记录已访问对象)。
-
性能权衡:
- 大型对象的深拷贝可能导致性能问题,需根据场景选择实现方式。
-
特殊对象处理:
- 如需保留
Date实例或Map结构,需手动处理或依赖第三方库。
- 如需保留
三、深浅拷贝的选择策略
1. 根据数据结构选择
- 浅拷贝:适用于简单数据结构或仅需修改第一层属性的场景。
- 深拷贝:适用于嵌套结构或需要完全独立的副本时。
2. 根据性能选择
- 浅拷贝:速度快,适合大型对象。
- 深拷贝:速度慢,但能保证数据独立性。
3. 根据数据类型选择
- JSON 方法:仅适合纯数据对象(无函数、Symbol)。
- 递归/第三方库:适合复杂数据类型(如嵌套对象、特殊对象)。
四、实际应用场景
1. React 中的状态管理
在 React 函数组件中,直接修改 state 会导致渲染问题。通过深拷贝更新状态,可避免副作用:
const [user, setUser] = useState({ name: "Alice", settings: { theme: "dark" } });
// 错误:直接修改 state
user.settings.theme = "light"; // ❌ 不会触发重渲染
// 正确:深拷贝后更新
setUser(JSON.parse(JSON.stringify(user)));
user.settings.theme = "light";
2. 表单数据的初始化
在表单组件中,深拷贝原始数据可避免用户输入污染默认值:
const defaultData = { name: "", age: 18 };
const [formData, setFormData] = useState(JSON.parse(JSON.stringify(defaultData)));
3. 配置对象的合并
合并用户配置与默认配置时,浅拷贝可能导致嵌套配置被覆盖:
const defaults = { timeout: 500, api: "/api" };
const userConfig = { timeout: 1000, debug: true };
const config = Object.assign({}, defaults, userConfig); // ❌ 嵌套配置未合并