Skip to main content

深拷贝

要求

  • 拷贝原始类型
  • 支持对象、数组、日期、正则、Map、Set 的拷贝
  • 处理 Symbol 作为键名的情况
  • 解决循环引用导致的递归爆栈/死循环问题

三种思路对比

方案优点缺点
JSON.parse(JSON.stringify())一行搞定丢失 undefined/函数/SymbolDate 变字符串,NaN/Infinitynull,无法处理循环引用
递归直观、易写层级过深会爆栈
循环(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
  • 数组:sliceconcatArray.from

注意:浅拷贝只复制第一层,嵌套对象仍是同一引用。

参考