深拷贝与浅拷贝的区别
浅拷贝:只复制对象的第一层属性。如果属性值是基本类型,则拷贝其值;如果是引用类型(对象、数组等),则拷贝其引用。因此,浅拷贝后的对象与原对象共享引用类型的属性,修改一方会影响另一方。
深拷贝:递归地复制对象的所有层级,生成一个完全独立的新对象,与原对象没有任何引用关系。修改深拷贝后的对象不会影响原对象。
可以把浅拷贝和深拷贝想象成复印文件和重新誊抄:
- 浅拷贝:就像复印一张表格,表格里的文字(基本类型)都复制下来了,但如果表格里夹着一张照片(引用类型),复印出来的文件里只是照片的复印件,而原照片只有一个。所以如果你在复印件上修改照片,原照片也会被改动,因为它们其实是同一张底片。
- 深拷贝:就像把表格里的每个字重新写一遍,连那张照片也单独重新冲洗一张。这样最终得到两份完全独立的文件,改哪一份都不会影响另一份。
示例
javascript
const original = {
a: 1,
b: { c: 2 }
};
const shallow = { ...original }; // 浅拷贝
shallow.b.c = 3;
console.log(original.b.c); // 3 — 原对象也被修改
const deep = JSON.parse(JSON.stringify(original)); // 一种深拷贝方式
deep.b.c = 4;
console.log(original.b.c); // 3 — 原对象不受影响
实现一个深拷贝函数(支持循环引用、Date、RegExp等)
以下实现覆盖了绝大多数常见场景,包括:
- 基本类型、函数(直接复用)
- 特殊对象:Date、RegExp、Map、Set、ArrayBuffer、TypedArray
- 循环引用(使用 WeakMap)
- 保留原型链
- 处理 Symbol 属性
- 数组与普通对象
javascript
function deepClone(value, cache = new WeakMap()) {
// 基本类型、null、undefined、函数直接返回
if (value === null || typeof value !== 'object') {
return value;
}
// 处理循环引用
if (cache.has(value)) {
return cache.get(value);
}
// 处理 Date
if (value instanceof Date) {
return new Date(value);
}
// 处理 RegExp
if (value instanceof RegExp) {
return new RegExp(value.source, value.flags);
}
// 处理 Map
if (value instanceof Map) {
const clonedMap = new Map();
cache.set(value, clonedMap);
for (const [key, val] of value) {
clonedMap.set(deepClone(key, cache), deepClone(val, cache));
}
return clonedMap;
}
// 处理 Set
if (value instanceof Set) {
const clonedSet = new Set();
cache.set(value, clonedSet);
for (const val of value) {
clonedSet.add(deepClone(val, cache));
}
return clonedSet;
}
// 处理 ArrayBuffer
if (value instanceof ArrayBuffer) {
return value.slice(0);
}
// 处理 TypedArray 和 DataView
if (ArrayBuffer.isView(value)) {
const buffer = value.buffer;
const clonedBuffer = buffer.slice(0);
// 根据不同类型创建新实例
const Constructor = value.constructor;
return new Constructor(clonedBuffer, value.byteOffset, value.length);
}
// 处理普通对象和数组:保留原型
const proto = Object.getPrototypeOf(value);
const cloned = Array.isArray(value) ? [] : Object.create(proto);
cache.set(value, cloned);
// 拷贝所有可枚举属性(包括 Symbol 属性)
const keys = [...Object.keys(value), ...Object.getOwnPropertySymbols(value)];
for (const key of keys) {
cloned[key] = deepClone(value[key], cache);
}
return cloned;
}
关键点说明
- 基本类型与函数:直接返回,函数体不会改变,浅拷贝即可。
- 循环引用:使用
WeakMap存储已拷贝的对象,在递归时检测到已存在则直接返回,避免无限递归。 - Date 和 RegExp:通过构造函数创建新实例,确保独立。
- Map 和 Set:递归克隆其键值对或元素。
- ArrayBuffer 与 TypedArray:复制底层缓冲区,再根据原类型构建新视图。
- 原型保留:通过
Object.create(proto)创建对象,保留原对象的原型链(例如自定义类的实例)。 - Symbol 属性:使用
Object.getOwnPropertySymbols获取并拷贝,保证属性完整性。 - 性能:
WeakMap可避免内存泄漏,且不阻止垃圾回收。
使用示例
javascript
const obj = {
num: 1,
str: 'hello',
date: new Date(),
regex: /abc/gi,
map: new Map([['a', 1]]),
set: new Set([1, 2]),
buffer: new ArrayBuffer(8),
uint8: new Uint8Array([1,2,3]),
nested: { x: 10 },
self: null
};
obj.self = obj; // 循环引用
const cloned = deepClone(obj);
console.log(cloned !== obj); // true
console.log(cloned.date !== obj.date); // true
console.log(cloned.regex !== obj.regex); // true
console.log(cloned.map !== obj.map); // true
console.log(cloned.set !== obj.set); // true
console.log(cloned.buffer !== obj.buffer); // true
console.log(cloned.uint8 !== obj.uint8); // true
console.log(cloned.self === cloned); // true (循环引用正常)
console.log(cloned.nested !== obj.nested); // true
注意事项
- 此实现不处理 DOM 元素、Error 对象等特殊宿主对象,若需支持可扩展对应分支。
- 函数、Symbol 作为属性值时已处理,但函数本身不深拷贝,通常已足够。
- 若需拷贝不可枚举属性或属性描述符,可使用
Object.getOwnPropertyDescriptors,但通常深拷贝仅关注数据。