深浅拷贝
在 JavaScript 中,数据拷贝是日常开发中频繁遇到的操作。但很多开发者在面对"拷贝" 时,常会陷入深浅拷贝的误区——明明复制了对象,却发现修改新对象时原对象也跟着变了。这就像复印文件时,浅拷贝只复印了封面,深拷贝才真正把内页也完整复制。今天我们就来彻底搞懂这两种拷贝方式,以及它们的实现方法。
浅拷贝
浅拷贝就像给物品套了个新盒子——只复制对象的表层结构,对于嵌套的子对象,新旧对象会共享同一份内存地址 。这意味着修改子对象时,原对象和拷贝对象会同时受到影响。
Object.assign()
Object.assign()
是 ES6 引入的浅拷贝方法,它会将源对象的可枚举属性复制到目标对象,并返回目标对象。
const obj = {
name: "张三",
info: {age: 20}
};
// 浅拷贝:只复制表层属性
const copyObj = Object.assign({}, obj);
// 修改表层属性,原对象不受影响
copyObj.name = "李四";
console.log(obj.name); // 输出:张三(原对象不变)
// 修改嵌套对象,原对象跟着变
copyObj.info.age = 21;
console.log(obj.info.age); // 输出:21(原对象被修改)
原理:Object.assign()
会遍历源对象的自有属性,将其值复制到目标对象。但对于引用类型的属性(如例子中的 info
对象),复制的是内存地址,而非对象本身——这就像两个盒子共用了同一个抽屉。
注意:
- 只拷贝自身属性,不拷贝继承属性和不可枚举属性
- 如果目标对象与源对象有同名属性,源对象属性会覆盖目标对象属性
结构符号(扩展运算符)
扩展运算符(...
)是一种更简洁的浅拷贝方式,语法上比 Object.assign()
更直观,常用于数组和对象的拷贝。
// 拷贝对象
const obj = {a: 1, b: {c: 2}};
const copyObj = {...obj};
// 拷贝数组
const arr = [1, 2, {3}];
const copyArr = [...arr];
// 测试嵌套对象
copyObj.b.c = 3;
console.log(obj.b.c); // 输出:3(原对象被修改)
原理:扩展运算符在拷贝时,会对对象/数组进行"一层遍历" ,将每个元素的值复制到新结构中。对于基本类型(数字、字符串等)会复制值,对于引用类型(对象、数组等)会复制地址——就像用相机拍集体照,只能拍到每个人的外表,拍不到口袋里的东西。
适用场景:
- 简单数据结构的快速拷贝
- 合并多个对象(如
{...obj1, ...obj2}
) - 函数参数的解构传递
深拷贝
深拷贝是"彻底的复制"——**不仅复制对象的表层结构,还会递归复制所有嵌套的子对象,新旧对象完全独立,修改任何一方都不会影响另一方 **。就像细胞分裂,新细胞拥有和母细胞完全相同的遗传物质,但之后会各自发展。
JSON.stringify()
JSON.stringify()
配合 JSON.parse()
是最常用的深拷贝方案,通过将对象转为 JSON 字符串,再解析回对象,实现完全独立的复制。
const obj = {
name: "张三",
info: {age: 20},
hobbies: ["篮球", "游戏"]
};
// 深拷贝:先转JSON字符串,再解析回对象
const deepCopy = JSON.parse(JSON.stringify(obj));
// 修改嵌套对象,原对象不受影响
deepCopy.info.age = 21;
deepCopy.hobbies.push("读书");
console.log(obj.info.age); // 输出:20(原对象不变)
console.log(obj.hobbies); // 输出:["篮球", "游戏"](原数组不变)
原理:JSON 字符串是纯文本格式,转换过程会剥离对象的引用关系。当重新解析为对象时,浏览器会为所有嵌套结构分配新的内存地址,实现彻底的独立。
局限性:
- 无法拷贝函数、正则表达式、日期对象(会被转为字符串)
- 无法处理循环引用(如
obj.self = obj
会报错) - 会忽略
undefined
和Symbol
类型的属性
MessageChannel(结构化克隆)
MessageChannel
是浏览器提供的通信 API,其内部使用"结构化克隆算法",能处理 JSON.stringify
无法拷贝的复杂类型(如循环引用、函数除外)。
function deepClone(obj) {
return new Promise((resolve) => {
const {port1, port2} = new MessageChannel();
// 通过端口发送对象,会自动触发结构化克隆
port2.onmessage = (e) => resolve(e.data);
port1.postMessage(obj);
});
}
// 使用示例
const obj = {
name: "张三",
date: new Date(),
self: null // 准备循环引用
};
obj.self = obj; // 循环引用:对象引用自身
// 注意:需要用async/await调用
(async () => {
const copy = await deepClone(obj);
console.log(copy.self === copy); // 输出:true(保留循环引用)
console.log(copy.date instanceof Date); // 输出:true(正确拷贝日期对象)
})();
原理:MessageChannel
的 postMessage
方法在传递数据时,会使用浏览器内置的结构化克隆算法,递归复制对象的所有层级,并保留复杂类型的特性。就像专业的文件复制工具,能识别并复制各种格式的文件。
支持的类型:
- 基本类型(除
Symbol
) - 日期、正则表达式
- 数组、对象
- 循环引用(对象引用自身)
局限性:
- 无法拷贝函数(会被忽略)
- 是异步操作,需要配合
Promise
使用 - 仅支持浏览器环境(Node.js 不支持)
手写深拷贝
实际开发中,我们可能需要一个自定义的深拷贝函数,灵活处理各种场景。手写深拷贝的核心是**递归遍历对象,对不同类型的数据采用不同的复制策略 **。
function deepClone(obj, hash = new WeakMap()) {
// 处理null和基本类型
if (obj === null || typeof obj !== "object") {
return obj;
}
// 处理循环引用
if (hash.has(obj)) {
return hash.get(obj);
}
let copy;
// 处理日期
if (obj instanceof Date) {
copy = new Date(obj);
hash.set(obj, copy);
return copy;
}
// 处理正则
if (obj instanceof RegExp) {
copy = new RegExp(obj.source, obj.flags);
hash.set(obj, copy);
return copy;
}
// 处理数组和对象
copy = Array.isArray(obj) ? [] : {};
hash.set(obj, copy); // 缓存已拷贝的对象,解决循环引用
// 递归拷贝属性
Reflect.ownKeys(obj).forEach(key => {
copy[key] = deepClone(obj[key], hash);
});
return copy;
}
// 测试
const obj = {
a: 1,
b: {c: 2},
d: new Date(),
e: /test/g,
self: null
};
obj.self = obj;
const copy = deepClone(obj);
console.log(copy.b.c); // 2
console.log(copy.self === copy); // true(正确处理循环引用)
console.log(copy.d instanceof Date); // true
核心逻辑:
- 类型判断:区分基本类型、对象、数组、日期等特殊类型
- 递归拷贝:对对象的每个属性递归调用深拷贝函数
- 循环引用处理:用
WeakMap
缓存已拷贝的对象,避免无限递归 - 特殊类型处理:为日期、正则等对象创建新实例,保留其特性
优势:
- 可自定义处理函数等特殊类型(如需拷贝函数,可添加
typeof obj === 'function'
的处理逻辑) - 同步操作,无需异步等待
- 全环境兼容(浏览器和 Node.js 均可使用)
总结
深浅拷贝的本质区别在于是否复制嵌套对象的引用:
- 浅拷贝:适合简单数据结构,效率高,但嵌套对象会共享引用
- 深拷贝:适合复杂数据结构,完全独立,但性能消耗更大
选择建议:
- 简单场景用
Object.assign()
或扩展运算符 - 需处理日期、循环引用时用
MessageChannel
- 需兼容函数、正则或自定义逻辑时,使用手写深拷贝
- 避免用
JSON.stringify
处理包含特殊类型的对象
掌握深浅拷贝,能帮你避免很多"修改新对象却影响原对象"的诡异 bug,让数据操作更可控。就像学会了不同的复制技巧,既能快速复印简单文件,也能完整备份复杂档案。