最近复习面试题,发现深拷贝几乎是必考题。
看起来一句 JSON.parse(JSON.stringify(obj)) 就能搞定,但面试官往往会追问:
JSON.parse(JSON.stringify(obj)) 存在什么问题? 怎么解决?
一、先搞清楚:什么是深拷贝
浅拷贝:只拷贝对象的第一层,嵌套对象是引用共享。
const obj = { a: 1, b: { c: 2 } };
const shallow = { ...obj };
shallow.b.c = 999;
console.log(obj.b.c); // 999 —— 原对象被改了
深拷贝:递归拷贝所有层级,新旧对象完全独立。
const deep = deepClone(obj);
deep.b.c = 999;
console.log(obj.b.c); // 2 —— 原对象没变
二、最简单的版本(5 行)
用 JSON 序列化再反序列化:
function deepClone(obj) {
return JSON.parse(JSON.stringify(obj));
}
能干活,但有 5 个坑:
undefined会丢失- 函数会丢失
Symbol会丢失Date会变成字符串- 循环引用会报错
验证一下:
const obj = {
a: undefined,
b: () => {},
c: Symbol('key'),
d: new Date(),
e: /abc/
};
console.log(JSON.parse(JSON.stringify(obj)));
// { d: '2026-09-15T...', e: {} }
// a、b、c 全没了,d 变成字符串,e 变成空对象
三、进阶版:递归实现
自己写递归,处理基本类型 + 对象 + 数组:
function deepClone(target) {
// 基本类型直接返回
if (target === null || typeof target !== 'object') {
return target;
}
// 数组和对象分开处理
const cloneTarget = Array.isArray(target) ? [] : {};
for (const key in target) {
if (target.hasOwnProperty(key)) {
cloneTarget[key] = deepClone(target[key]);
}
}
return cloneTarget;
}
改进点:
- ✅ 支持
undefined、函数、Symbol 值 - ✅ 支持嵌套对象/数组
- ❌ 循环引用会栈溢出
- ❌ Date、RegExp、Map、Set 还是没处理
四、处理循环引用:用 WeakMap
先看什么是循环引用:
const obj = { a: 1 };
obj.self = obj; // obj 引用了自己
deepClone(obj); // 栈溢出 RangeError
解法:用一个 WeakMap 记录已经拷贝过的对象,遇到重复的直接返回。
function deepClone(target, map = new WeakMap()) {
if (target === null || typeof target !== 'object') {
return target;
}
// 如果已经拷贝过,直接返回
if (map.has(target)) {
return map.get(target);
}
const cloneTarget = Array.isArray(target) ? [] : {};
// 先记录,再递归(顺序很重要)
map.set(target, cloneTarget);
for (const key in target) {
if (target.hasOwnProperty(key)) {
cloneTarget[key] = deepClone(target[key], map);
}
}
return cloneTarget;
}
为什么用 WeakMap 而不是 Map?
WeakMap的 key 是弱引用,不阻止 GC 回收- 拷贝结束后自动释放内存,避免内存泄漏
顺序为什么关键?
一定要先 map.set,再递归。如果先递归,遇到循环引用时 map 还没记录,就又会栈溢出。
五、处理特殊类型:Date、RegExp、Map、Set
上面的版本对 Date 等对象的处理是错的:
deepClone(new Date());
// {} —— 变成空对象了
原因:for...in 遍历不到 Date 的内部属性,得根据类型单独处理。
function deepClone(target, map = new WeakMap()) {
// 基本类型
if (target === null || typeof target !== 'object') {
return target;
}
// 循环引用
if (map.has(target)) {
return map.get(target);
}
// Date
if (target instanceof Date) {
return new Date(target.getTime());
}
// RegExp
if (target instanceof RegExp) {
return new RegExp(target.source, target.flags);
}
// Map
if (target instanceof Map) {
const result = new Map();
map.set(target, result);
target.forEach((value, key) => {
result.set(key, deepClone(value, map));
});
return result;
}
// Set
if (target instanceof Set) {
const result = new Set();
map.set(target, result);
target.forEach(value => {
result.add(deepClone(value, map));
});
return result;
}
// 普通对象/数组
const cloneTarget = Array.isArray(target) ? [] : {};
map.set(target, cloneTarget);
for (const key in target) {
if (target.hasOwnProperty(key)) {
cloneTarget[key] = deepClone(target[key], map);
}
}
return cloneTarget;
}
六、处理 Symbol key
for...in 只能遍历字符串 key,遍历不到 Symbol key:
const key = Symbol('key');
const obj = { [key]: 1 };
for (const k in obj) console.log(k); // 什么都不打印
解决:用 Reflect.ownKeys() 遍历(能拿到字符串 key + Symbol key):
Reflect.ownKeys(target).forEach(key => {
cloneTarget[key] = deepClone(target[key], map);
});
七、终极完整版
把上面所有优化合并:
function deepClone(target, map = new WeakMap()) {
if (target === null || typeof target !== 'object') return target;
if (map.has(target)) return map.get(target);
if (target instanceof Date) return new Date(target.getTime());
if (target instanceof RegExp) return new RegExp(target.source, target.flags);
if (target instanceof Map) {
const result = new Map();
map.set(target, result);
target.forEach((v, k) => result.set(k, deepClone(v, map)));
return result;
}
if (target instanceof Set) {
const result = new Set();
map.set(target, result);
target.forEach(v => result.add(deepClone(v, map)));
return result;
}
const cloneTarget = Array.isArray(target) ? [] : {};
map.set(target, cloneTarget);
Reflect.ownKeys(target).forEach(key => {
cloneTarget[key] = deepClone(target[key], map);
});
return cloneTarget;
}
测试一下:
const obj = {
a: undefined,
b: () => 'fn',
c: new Date(),
d: /abc/i,
e: new Map([['k', 'v']]),
f: new Set([1, 2]),
[Symbol('key')]: 'symbol',
};
obj.self = obj; // 循环引用
const cloned = deepClone(obj);
console.log(cloned.self === cloned); // true —— 循环引用保留
console.log(cloned.c instanceof Date); // true
console.log(cloned.d.source); // 'abc'
console.log(cloned.e.get('k')); // 'v'
八、如果不想手写,还有 2 个选择
方案 1 · Lodash 的 _.cloneDeep
import _ from 'lodash';
const cloned = _.cloneDeep(obj);
方案 2 · 浏览器原生 structuredClone
const cloned = structuredClone(obj);
structuredClone 支持大部分复杂类型,包括循环引用。
但不支持:
- 函数(会抛错)
- Symbol(会抛错)
- DOM 节点
- 原型链
面试八股题里如果面试官问"有原生 API 吗",答 structuredClone 是加分项。
总结
深拷贝这道题的层次:
- JSON 大法:5 行,能干活但有很多坑
- 递归实现:处理普通对象/数组
- 循环引用:用 WeakMap 记录
- 特殊类型:Date/RegExp/Map/Set 单独处理
- Symbol key:用
Reflect.ownKeys遍历
面试怎么答:
- 面试官问"写个深拷贝"→ 从简单版本开始
- 追问"有什么问题"→ 指出 5 个坑
- 继续追问 → 逐步给出上面的版本
- 不用一上来就写终极版本,展示"思考过程"更重要
如果觉得有帮助,点个赞或关注,后续会继续更新面试题深度解析系列。