深拷贝与浅拷贝的区别
在 JavaScript 的开发与面试中,深拷贝(Deep Copy)与浅拷贝(Shallow Copy)是无法绕开的高频考点。这不仅关乎数据的安全性,更直接体现了开发者对 JavaScript 内存管理模型的理解深度。本文将从底层原理出发,剖析两者的区别、实现方式及最佳实践。
一、 引言:内存中的栈与堆
要理解拷贝,首先必须理解 JavaScript 的数据存储方式。JavaScript 的数据类型分为两类:
- 基本数据类型(Number, String, Boolean, Null, Undefined, Symbol, BigInt):这些类型的值较小且固定,直接存储在栈内存(Stack)中。
- 引用数据类型(Object, Array, Function, Date 等):这些类型的值大小不固定,实体存储在堆内存(Heap)中,而在栈内存中存储的是一个指向堆内存实体的地址(指针) 。
当我们进行赋值操作(=)时:
- 基本类型赋值的是值本身。
- 引用类型赋值的是内存地址。
这就是深浅拷贝问题的根源:我们究竟是复制了指针,还是复制了实体?
二、 浅拷贝(Shallow Copy)详解
1. 定义
浅拷贝是创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。
- 如果属性是基本类型,拷贝的就是基本类型的值。
- 如果属性是引用类型,拷贝的就是内存地址。
- 核心结论:浅拷贝只复制对象的第一层,对于嵌套的对象,新旧对象共享同一块堆内存。
2. 常用实现方式
- Object.assign()
- 展开运算符 ...
- Array.prototype.slice() / concat()
3. 代码演示与现象
JavaScript
const source = {
name: 'Juejin',
info: {
age: 10,
city: 'Beijing'
}
};
// 使用展开运算符实现浅拷贝
const target = { ...source };
// 1. 修改第一层属性(基本类型)
target.name = 'Google';
console.log(source.name); // 输出: 'Juejin'
console.log(target.name); // 输出: 'Google'
// 结论:第一层互不影响
// 2. 修改嵌套层属性(引用类型)
target.info.age = 20;
console.log(source.info.age); // 输出: 20
console.log(target.info.age); // 输出: 20
// 结论:嵌套层共享引用,牵一发而动全身
三、 深拷贝(Deep Copy)详解
1. 定义
深拷贝是将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象。无论嵌套多少层,新旧对象在内存上都是完全独立的。
2. 常用实现方式
方案 A:JSON.parse(JSON.stringify())
这是最简单的深拷贝方法,适用于纯数据对象(Plain Object)。
局限性:
- 无法处理 undefined、Symbol 和函数(会丢失)。
- 无法处理循环引用(会报错)。
- 无法正确处理 Date(变字符串)、RegExp(变空对象)等特殊对象。
JavaScript
const source = {
a: 1,
b: { c: 2 }
};
const target = JSON.parse(JSON.stringify(source));
方案 B:递归实现(简易版)
通过递归遍历对象属性,如果是引用类型则再次调用拷贝函数。
JavaScript
function deepClone(obj) {
// 处理 null 和基本类型
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 初始化返回结果,兼容数组和对象
let result = Array.isArray(obj) ? [] : {};
for (let key in obj) {
// 保证只拷贝自身可枚举属性
if (obj.hasOwnProperty(key)) {
// 递归拷贝
result[key] = deepClone(obj[key]);
}
}
return result;
}
方案 C:Web API - structuredClone
现代浏览器原生支持的深拷贝 API,支持循环引用,性能优于 JSON 序列化,但不支持函数和部分 DOM 节点。
JavaScript
const target = structuredClone(source);
3. 演示现象
JavaScript
const source = {
info: {
age: 10
}
};
// 使用手写递归实现深拷贝
const target = deepClone(source);
target.info.age = 999;
console.log(source.info.age); // 输出: 10
console.log(target.info.age); // 输出: 999
// 结论:完全独立,互不干扰
四、 特点总结
| 特性 | 浅拷贝 (Shallow Copy) | 深拷贝 (Deep Copy) |
|---|---|---|
| 内存分配 | 仅第一层开辟新空间,嵌套层共享地址 | 所有层级均开辟新空间,完全独立 |
| 执行速度 | 快 | 慢(取决于层级深度和数据量) |
| 实现难度 | 简单(原生语法支持) | 复杂(需处理循环引用、特殊类型) |
| 适用场景 | 状态更新、合并配置、一般的数据处理 | 复杂数据备份、防止副作用修改、Redux/Vuex 状态管理 |
五、 面试高分指南
当面试官问到:“请你说一下深拷贝和浅拷贝的区别,以及如何实现? ”时,建议按照以下逻辑结构回答,展示系统化的思维。
1. 从内存模型切入
“首先,这涉及到 JavaScript 的内存存储机制。基本数据类型存储在栈中,引用数据类型存储在堆中。
浅拷贝和深拷贝的主要区别在于复制的是引用地址还是堆内存中的实体数据。”
2. 阐述核心区别
“浅拷贝只复制对象的第一层属性。如果属性是基本类型,拷贝的是值;如果是引用类型,拷贝的是内存地址。因此,修改新对象的嵌套属性会影响原对象。
深拷贝则是递归地复制所有层级,在堆内存中开辟新的空间。新旧对象在物理内存上是完全隔离的,修改任何一方都不会影响另一方。”
3. 列举实现方案
“在实际开发中:
- 浅拷贝通常使用 Object.assign() 或 ES6 的展开运算符 ...。
- 深拷贝最简单的方式是 JSON.parse(JSON.stringify()),但它有忽略 undefined、函数以及无法处理循环引用的缺陷。
- 现代环境下,推荐使用 structuredClone API。
- 在需要兼容性或处理复杂逻辑时,通常使用 Lodash 的 _.cloneDeep 或手写递归函数。”
4. 进阶亮点(加分项)
“如果需要手写一个完善的深拷贝,需要注意两个关键点:
第一,解决循环引用。比如对象 A 引用了 B,B 又引用了 A,直接递归会导致栈溢出。解决方案是使用 WeakMap 作为哈希表,存储已拷贝过的对象。每次拷贝前先检查 WeakMap,如果存在则直接返回,不再递归。
第二,处理特殊类型。除了普通对象和数组,还需要考虑 Date、RegExp、Map、Set 等类型,不能简单地通过 new obj.constructor() 处理,需要针对性地获取它们的值进行重建。”