对象深拷贝
完整实现
function deepClone(obj, hash = new WeakMap()) {
// 1. 处理基本类型和 null
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 2. 处理循环引用
if (hash.has(obj)) {
return hash.get(obj);
}
// 3. 处理特殊对象类型
// 3.1 Date 对象
if (obj instanceof Date) {
return new Date(obj);
}
// 3.2 RegExp 对象
if (obj instanceof RegExp) {
const flags = obj.flags;
const pattern = obj.source;
return new RegExp(pattern, flags);
}
// 3.3 Map 对象
if (obj instanceof Map) {
const mapCopy = new Map();
hash.set(obj, mapCopy);
obj.forEach((value, key) => {
mapCopy.set(deepClone(key, hash), deepClone(value, hash));
});
return mapCopy;
}
// 3.4 Set 对象
if (obj instanceof Set) {
const setCopy = new Set();
hash.set(obj, setCopy);
obj.forEach(value => {
setCopy.add(deepClone(value, hash));
});
return setCopy;
}
// 3.5 Array 对象
if (obj instanceof Array) {
const arrCopy = [];
hash.set(obj, arrCopy);
obj.forEach((item, index) => {
arrCopy[index] = deepClone(item, hash);
});
return arrCopy;
}
// 3.6 普通对象
if (obj instanceof Object) {
// 获取对象的所有属性描述符(包括不可枚举属性和 Symbol 属性)
const allDescriptors = Object.getOwnPropertyDescriptors(obj);
const objCopy = Object.create(Object.getPrototypeOf(obj), allDescriptors);
hash.set(obj, objCopy);
// 遍历所有属性(包括 Symbol 属性)
[...Object.keys(obj), ...Object.getOwnPropertySymbols(obj)].forEach(key => {
objCopy[key] = deepClone(obj[key], hash);
});
return objCopy;
}
// 4. 如果都不匹配,返回原值
return obj;
}
详细原理讲解
1. 基本类型处理
if (obj === null || typeof obj !== 'object') {
return obj;
}
- 原理:JavaScript 的基本类型(string、number、boolean、undefined、symbol)和 null 是直接存储在栈内存中的值,赋值是值拷贝,所以直接返回原值即可
- 为什么检查 null:
typeof null === 'object'是 JavaScript 的历史遗留 bug,需要特殊处理
2. 循环引用处理
if (hash.has(obj)) {
return hash.get(obj);
}
// ... 在创建新对象后
hash.set(obj, objCopy);
- 原理:使用 WeakMap 存储已经克隆过的对象,key 是原对象,value 是克隆对象
- 为什么用 WeakMap:WeakMap 的 key 是弱引用,不会阻止垃圾回收,避免内存泄漏
- 解决什么问题:防止对象循环引用时导致无限递归,如
a.self = a
3. 特殊对象类型处理
3.1 Date 对象
if (obj instanceof Date) {
return new Date(obj);
}
- 原理:Date 对象存储的是时间戳,通过
new Date(originalDate)可以创建具有相同时间的新 Date 对象 - 为什么不能直接赋值:直接赋值复制的是引用,修改副本会影响原对象
3.2 RegExp 对象
if (obj instanceof RegExp) {
const flags = obj.flags;
const pattern = obj.source;
return new RegExp(pattern, flags);
}
- 原理:正则表达式由 pattern(模式)和 flags(标志)组成
- flags 属性:ES6 新增,返回正则表达式的标志字符串(如 'g', 'i', 'm' 等)
- source 属性:返回正则表达式的模式文本
3.3 Map 对象
if (obj instanceof Map) {
const mapCopy = new Map();
hash.set(obj, mapCopy);
obj.forEach((value, key) => {
mapCopy.set(deepClone(key, hash), deepClone(value, hash));
});
return mapCopy;
}
- 原理:Map 的 key 和 value 都可能是引用类型,都需要深拷贝
- 关键点:必须先创建空 Map 并存入 hash,再遍历填充,处理循环引用
3.4 Set 对象
if (obj instanceof Set) {
const setCopy = new Set();
hash.set(obj, setCopy);
obj.forEach(value => {
setCopy.add(deepClone(value, hash));
});
return setCopy;
}
- 原理:Set 的元素可能是引用类型,需要深拷贝
- 注意:Set 的元素是唯一的,克隆时要保持这种唯一性
3.5 Array 对象
if (obj instanceof Array) {
const arrCopy = [];
hash.set(obj, arrCopy);
obj.forEach((item, index) => {
arrCopy[index] = deepClone(item, hash);
});
return arrCopy;
}
- 原理:数组是特殊的对象,需要保持索引顺序
- 为什么用 forEach:保留空位(sparse array),map 会跳过空位
4. 普通对象的高级处理
if (obj instanceof Object) {
// 获取所有属性描述符
const allDescriptors = Object.getOwnPropertyDescriptors(obj);
// 创建具有相同原型和属性描述符的新对象
const objCopy = Object.create(Object.getPrototypeOf(obj), allDescriptors);
hash.set(obj, objCopy);
// 遍历所有属性进行深拷贝
[...Object.keys(obj), ...Object.getOwnPropertySymbols(obj)].forEach(key => {
objCopy[key] = deepClone(obj[key], hash);
});
return objCopy;
}
这部分的详细原理:
4.1 保留原型链
Object.getPrototypeOf(obj)
- 获取对象的原型,确保克隆对象与原对象有相同的原型链
4.2 保留属性描述符
Object.getOwnPropertyDescriptors(obj)
- 获取对象所有属性的完整描述符(value、writable、enumerable、configurable、get、set)
- 确保克隆对象保留原属性的特性,包括 getter/setter
4.3 创建新对象
Object.create(proto, descriptors)
- 第一个参数:指定新对象的原型
- 第二个参数:属性描述符对象,直接设置到新对象上
- 这样创建的对象完美继承了原对象的所有特性
4.4 处理 Symbol 属性
[...Object.keys(obj), ...Object.getOwnPropertySymbols(obj)]
Object.keys()获取可枚举的字符串属性Object.getOwnPropertySymbols()获取所有 Symbol 属性- 展开运算符组合两者,确保所有类型的属性都被克隆
使用示例
// 测试数据
const obj = {
num: 1,
str: 'hello',
bool: true,
date: new Date(),
reg: /test/gi,
arr: [1, 2, { nested: 'object' }],
map: new Map([['key', 'value']]),
set: new Set([1, 2, 3]),
[Symbol('sym')]: 'symbol property',
get fullName() {
return this.firstName + ' ' + this.lastName;
}
};
// 循环引用测试
obj.self = obj;
// 执行克隆
const cloned = deepClone(obj);
// 验证
console.log(cloned !== obj); // true
console.log(cloned.date !== obj.date); // true
console.log(cloned.reg !== obj.reg); // true
console.log(cloned.arr[2] !== obj.arr[2]); // true
console.log(cloned.fullName); // undefined(getter 被正确复制但需要上下文)
局限性说明
- 函数:一般保持引用不变,因为函数的作用域链无法复制
- DOM 节点:需要特殊处理,通常使用
cloneNode() - 内置对象:如 Buffer、Promise 等需要特殊处理
- WeakMap/WeakSet:无法遍历,无法复制
这个实现覆盖了 95% 以上的日常使用场景,是一个工业级的深拷贝函数。
对象深拷贝
http://localhost:8090/archives/dui-xiang-shen-kao-bei