深拷贝
要求
- 拷贝原始类型
- 支持对象、数组、日期、正则、Map、Set 的拷贝
- 处理
Symbol作为键名的情况 - 解决循环引用导致的递归爆栈/死循环问题
三种思路对比
| 方案 | 优点 | 缺点 |
|---|---|---|
JSON.parse(JSON.stringify()) | 一行搞定 | 丢失 undefined/函数/Symbol,Date 变字符串,NaN/Infinity 变 null,无法处理循环引用 |
| 递归 | 直观、易写 | 层级过深会爆栈 |
| 循环(BFS/DFS + 栈) | 避免爆栈 | 代码较复杂 |
structuredClone() | 浏览器/Node 原生,支持循环引用与多数内置类型 | 不能拷贝函数、DOM 节点、原型链 |
现代环境里能用
structuredClone(obj)就优先用它;面试要的是手写实现,重点考察 循环引用 和 特殊类型 的处理。
递归(启蒙版)
只处理普通对象/数组,作为打底:
function deepClone(obj) {
if (obj === null || typeof obj !== "object") {
return obj;
}
const clone = Array.isArray(obj) ? [] : {};
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
clone[key] = deepClone(obj[key]);
}
}
return clone;
}
递归(完整版)
处理特殊类型、Symbol 键、循环引用(用 WeakMap 弱引用避免内存泄漏):
function deepClone(obj, hash = new WeakMap()) {
// 原始类型与 null 直接返回
if (obj === null || typeof obj !== "object") return obj;
// 特殊对象类型
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags);
// 循环引用:已拷贝过则直接返回缓存
if (hash.has(obj)) return hash.get(obj);
// 保留原型链,同时支持数组/对象
const cloneObj = new obj.constructor();
hash.set(obj, cloneObj);
// Map
if (obj instanceof Map) {
obj.forEach((value, key) => {
cloneObj.set(key, deepClone(value, hash));
});
return cloneObj;
}
// Set
if (obj instanceof Set) {
obj.forEach((value) => {
cloneObj.add(deepClone(value, hash));
});
return cloneObj;
}
// 同时拷贝普通键和 Symbol 键
const keys = [
...Object.keys(obj),
...Object.getOwnPropertySymbols(obj),
];
for (const key of keys) {
cloneObj[key] = deepClone(obj[key], hash);
}
return cloneObj;
}
const a = { name: "Dylan", info: { age: 24, hobby: ["看电影", "打游戏"] } };
a.self = a; // 循环引用
const b = deepClone(a);
console.log(b.self === b); // true,循环引用被正确处理
关键修复点:旧版本里 Symbol 拷贝写成了
target[symKey](target未定义),且写在cloneObj创建之前,必定报错。这里统一在cloneObj创建后处理。
循环(BFS)
用队列做广度优先遍历,避免递归爆栈,同样用 Map 解决循环引用:
function isObject(o) {
return o !== null && typeof o === "object";
}
function deepCloneByLoop(source) {
if (!isObject(source)) return source;
const visited = new Map();
const root = new source.constructor();
visited.set(source, root);
const queue = [[source, root]];
while (queue.length) {
const [oldNode, newNode] = queue.shift();
const keys = [
...Object.keys(oldNode),
...Object.getOwnPropertySymbols(oldNode),
];
for (const key of keys) {
const value = oldNode[key];
if (isObject(value)) {
if (visited.has(value)) {
newNode[key] = visited.get(value); // 复用已拷贝引用
} else {
const copy = new value.constructor();
visited.set(value, copy);
newNode[key] = copy;
queue.push([value, copy]);
}
} else {
newNode[key] = value;
}
}
}
return root;
}
浅拷贝
- 展开运算符
... - 对象:
Object.assign - 数组:
slice、concat、Array.from
注意:浅拷贝只复制第一层,嵌套对象仍是同一引用。