今天在开发过程中编写代码时,没想到在深浅拷贝的问题上栽了跟头,因此特意重新梳理了相关知识。下面跟我复习一下关于深浅拷贝吧!
一、为什么需要深浅拷贝?
要理解深浅拷贝,首先要明白JavaScript的数据类型存储机制:
-
基本类型:Number、String、Boolean等,直接存储在栈内存中
-
引用类型:Object、Array等,地址存储在栈内存,真实数据在堆内存
举个简单的例子:
const arr = [1, 2, 3];
const newArr = arr;
newArr.push(4);
console.log(arr); // 输出 [1, 2, 3, 4]
这里newArr和arr指向同一块内存地址,修改newArr会直接影响arr。这就是典型的引用传递带来的副作用。要避免这种问题,就必须掌握正确的拷贝姿势。深浅拷贝的出现就是为了解决这个问题:浅拷贝复制一层引用,深拷贝则递归复制所有层级。
二、浅拷贝,浅拷贝复制一层属性
浅拷贝的本质
浅拷贝会创建新对象,复制原始对象属性值。当属性是基本类型时直接复制值,引用类型时复制内存地址。
常见实现方法
1. Object.assign()
const obj = { a: 1, b: { c: 2 } };
const copy = Object.assign({}, obj);
copy.b.c = 3;
console.log(obj.b.c); // 输出 3
优缺点分析
- 优点:
- 简单易用,无需额外代码
- 明确语义(ES6标准方法)
- 缺点:
- 无法处理嵌套引用类型
- 仅拷贝对象自身可枚举属性
- 不保留原型链属性
// 示例:原型链属性丢失
const parent = { a: 1 };
const obj = Object.create(parent, {
b: { value: 2, enumerable: true }
});
const copy = Object.assign({}, obj);
console.log(copy.a); // undefined(原型链属性未拷贝)
2. 展开运算符(...)
const arr = [1, [2, 3]];
const copy = [...arr];
copy[1][0] = 4;
console.log(arr[1][0]); // 输出 4
优缺点分析
- 优点:
- 语法简洁直观
- 支持对象和数组
- 缺点:
- 无法处理嵌套引用
- 不保留构造函数信息
3. 数组方法(concat、slice)
const arr = [1, [2, 3]];
const copy = arr.concat();
copy[1][0] = 4;
console.log(arr[1][0]); // 输出 4
优缺点分析
- 优点:
- ES5兼容性好
- 明确数组合并语义
- 缺点:
- 仅限数组使用
- 语义不直观
浅拷贝有一个致命缺陷,遇到嵌套对象时就会暴露问题。例如:
const obj = {
name: '张三',
family: {
child: '小张三'
}
};
const copy = { ...obj };
copy.family.child = '大张三';
console.log(obj.family.child); // 输出 '大张三'
此时修改拷贝对象的family属性,原始对象也会被修改,这就是典型的浅拷贝陷阱。
三、深拷贝:递归复制所有层级
深拷贝的本质
深拷贝会递归遍历对象的所有层级,为每个引用类型创建新的内存空间。这样新对象和原对象完全独立,互不影响。
常见实现方法
1. JSON大法
const obj = { a: 1, b: { c: 2 } };
const copy = JSON.parse(JSON.stringify(obj));
copy.b.c = 3;
console.log(obj.b.c); // 输出 2
优缺点分析:
-
优点:简单易用,无需额外代码
-
缺点:
- 无法处理循环引用
- 忽略特殊类型
- Date对象转为字符串
- 性能低下(大数据量时明显卡顿)
//无法处理循环引用
const obj = { a: null };
obj.a = obj;
JSON.stringify(obj); // TypeError: Converting circular structure to JSON
//忽略特殊类型
const data = {
fn: () => {},
sym: Symbol(),
undef: undefined
};
const copy = JSON.parse(JSON.stringify(data));
console.log(copy); // {}(函数/Symbol/undefined丢失)
//Date对象转为字符串
const obj = { date: new Date() };
const copy = JSON.parse(JSON.stringify(obj));
console.log(typeof obj.date); // object
console.log(typeof copy.date); // string
2. 递归实现
function deepClone(obj) {
if (typeof obj !== 'object' || obj === null) return obj;
const isArray = Array.isArray(obj);
const copy = isArray ? [] : {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
copy[key] = deepClone(obj[key]);
}
}
return copy;
}
增强递归实现
优化点:
- 处理数组和对象的区别
- 处理循环引用(使用 WeakMap)
- 支持特殊对象(Date、RegExp 等)
function deepClone(obj, map = new WeakMap()) {
if (typeof obj !== 'object' || obj === null) return obj;
if (map.has(obj)) return map.get(obj);
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
const isArray = Array.isArray(obj);
const copy = isArray ? [] : {};
map.set(obj, copy);
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
copy[key] = deepClone(obj[key], map);
}
}
return copy;
}
3. 第三方库实现
Lodash的_.cloneDeep是最佳实践:
import _ from 'lodash';
const obj = { a: 1, b: { c: 2 } };
const copy = _.cloneDeep(obj);
copy.b.c = 3;
console.log(obj.b.c); // 输出 2
优缺点分析
- 优点:
- 开箱即用,处理所有边缘情况
- 支持复杂类型(Map/Set等)
- 缺点:
- 需引入第三方库,增加项目体积
4.现代浏览器原生API
const obj = { a: 1, b: { c: 2 } };
const copy = structuredClone(obj);
copy.b.c = 3;
console.log(obj.b.c); // 输出 2
支持处理更多数据类型,但是要注意兼容性的问题
四、该用哪种拷贝方式?
浅拷贝方法对比
| 方法 | 适用场景 | 代码示例 | 优点 | 局限性 |
|---|---|---|---|---|
| Object.assign() | 对象拷贝 | Object.assign({}, obj) | 明确语义,ES6标准方法 | 只能处理对象,需空对象占位 |
| 展开运算符 | 对象/数组拷贝 | {...obj} / [...arr] | 语法简洁,直观易读 | 无法处理深层嵌套 |
| Array.prototype.concat | 数组拷贝 | [].concat(arr) | 兼容性好(ES5) | 仅限数组,语义不直观 |
| Array.prototype.slice | 数组拷贝 | arr.slice() | 零参数快速拷贝 | 方法名易与截取操作混淆 |
深拷贝方法对比
| 方法 | 适用场景 | 代码示例 | 优点 | 局限性 |
|---|---|---|---|---|
| JSON方法 | 纯数据对象临时拷贝 | JSON.parse(JSON.stringify(obj)) | 实现简单、无需第三方库 | 丢失函数/Symbol/undefined;Date转为字符串;无法处理循环引用 |
| 基础递归 | 学习/简单对象拷贝 | function clone(o){...递归逻辑} | 理解原理、自定义灵活 | 无法处理循环引用;不处理特殊对象(Date/RegExp等);Symbol键名丢失 |
| 增强递归 | 需要保留特殊类型的自研场景 | 递归+WeakMap+类型判断 | 支持循环引用和特殊类型 | 开发成本高;性能较差;需手动维护类型判断逻辑 |
| lodash.cloneDeep | 生产环境复杂业务数据 | import _ from 'lodash'; _.cloneDeep(obj) | 开箱即用;处理所有边缘情况 | 增加项目体积(约17KB);需引入第三方库 |
| structuredClone | 现代浏览器环境 | const copy = structuredClone(obj) | 原生API性能最佳 |
这么多的方法,在具体使用时,该怎么选择? 我们需要根据具体场景选择合适的方法:
- 简单对象 ➡️ 扩展运算符
- 需要保留方法 ➡️ 递归+类型判断
- 复杂业务场景 ➡️ lodash.cloneDeep
- 现代浏览器环境 ➡️ structuredClone
五、总结
深浅拷贝是 JavaScript 中处理数据复制的核心概念。浅拷贝适用于单层对象,深拷贝则用于复杂数据结构。
| 对比维度 | 浅拷贝 | 深拷贝 |
|---|---|---|
| 拷贝层级 | 仅第一层 | 递归所有层级 |
| 内存引用 | 嵌套对象仍共享内存地址 | 完全独立的内存空间 |
| 性能 | 高(仅复制一层) | 低(递归遍历消耗资源) |
| 适用场景 | 简单数据隔离 | 复杂数据完全隔离 |
| 典型问题 | 修改嵌套属性会影响原对象 | 处理循环引用和特殊对象较复杂 |
结尾互动:
你在开发中遇到过哪些因深浅拷贝导致的 “玄学问题”?欢迎在评论区分享你的踩坑经历,或提出关于拷贝性能优化的疑问,一起讨论交流!如果觉得本文对你有帮助,别忘了点赞收藏,让更多开发者看到这份深浅拷贝避坑指南~ 😊