【前端面试小册】JS-第18节:深拷贝进阶版
第18节:深拷贝进阶版
一、深拷贝的概念
1.1 什么是深拷贝
深拷贝是指创建一个新对象,新对象的所有属性都是原对象属性的完全独立副本,修改新对象不会影响原对象。
对比浅拷贝:
- 浅拷贝:只复制第一层,嵌套对象仍然共享引用
- 深拷贝:递归复制所有层级,完全独立
// 浅拷贝
const obj = { a: 1, b: { c: 2 } };
const shallow = { ...obj };
shallow.b.c = 3;
console.log(obj.b.c); // 3(被修改了)
// 深拷贝
const deep = deepCopy(obj);
deep.b.c = 3;
console.log(obj.b.c); // 2(未被修改)
二、方法一:JSON.stringify()
2.1 基本实现
function deepCopy(target) {
return JSON.parse(JSON.stringify(target));
}
2.2 使用示例
let obj = {
a: 0,
b: {
c: 0,
}
};
const objCopy = deepCopy(obj);
obj.a = 1;
obj.b.c = 1;
console.log(obj); // { a: 1, b: { c: 1 } }
console.log(objCopy); // { a: 0, b: { c: 0 } }
// 上面改变源对象 c 的值不会影响复制产生的对象
2.3 JSON.stringify 的问题
局限性:
- 忽略
undefined、symbol、无法拷贝函数 - 不能正确处理
RegExp、Date、Set、Map - 不能处理正则
- 循环引用会导致无限递归,栈溢出
2.4 问题演示
let obj_1 = {
a: 0,
b: undefined,
c: Symbol(2),
d: new RegExp(/a/g),
e: new Set([1, 1]),
f: new Map([['age', 2]])
};
let obj_1_Copy = deepCopy(obj_1);
console.log(obj_1_Copy); // { a: 0, d: {}, e: {}, f: {} }
可以看到拷贝后的值有以下问题:
undefined的b直接被忽略Symbol的c被忽略- 正则
d变成了空对象{} Set的e变成了空对象{}Map的f变成了空对象{}
三、方法二:简单递归实现
3.1 基本实现
function deepCopy(target) {
if (target instanceof Object) {
// 处理函数
if (target instanceof Function) {
return function() {
return target.call(this, ...arguments);
};
}
// 处理正则
if (target instanceof RegExp) {
return new RegExp(target.source, target.flags);
}
// 处理日期
if (target instanceof Date) {
return new Date(target);
}
// 处理 Map
if (target instanceof Map) {
return new Map(target);
}
// 处理 Set
if (target instanceof Set) {
return new Set(target);
}
// 处理数组和对象
const copy = Array.isArray(target) ? [] : {};
for (const key in target) {
if (target.hasOwnProperty(key)) {
copy[key] = deepCopy(target[key]);
}
}
return copy;
}
return target;
}
3.2 使用示例
const obj = {
bigInt: BigInt(22),
symbol: Symbol(2),
set: new Set([1]),
map: new Map([[1, 2]]),
};
const objCopy = deepCopy(obj);
objCopy.set.add(88);
console.log('obj', obj);
// {
// bigInt: 22n,
// symbol: Symbol(2),
// set: Set(1) { 1 },
// map: Map(1) { 1 => 2 }
// }
console.log('objCopy', objCopy);
// {
// bigInt: 22n,
// symbol: Symbol(2),
// set: Set(2) { 1, 88 },
// map: Map(1) { 1 => 2 }
// }
// 发现复制产生的对象不受影响
3.3 问题:循环引用
const obj = {
a: 1,
b: undefined,
c: {
name: '愚公上岸说'
},
d: [2]
};
obj.obj = obj; // 循环引用
const copy = deepCopy(obj);
// 循环引用导致栈溢出:Maximum call stack size exceeded
问题分析:
graph TD
A[开始拷贝 obj] --> B[拷贝属性 a]
B --> C[拷贝属性 c]
C --> D[拷贝 c.name]
D --> E[拷贝属性 obj]
E --> F[发现 obj 指向原对象]
F --> G[再次拷贝 obj]
G --> H[无限递归]
H --> I[栈溢出]
四、方法三:终极版(防止循环引用)
4.1 核心思路
解决循环引用问题,额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系。
当需要拷贝当前对象时,先去存储空间中找,如果拷贝过的话直接返回;如果没有的话继续拷贝,这样就解决了循环引用的问题。
4.2 完整实现
// 克隆函数
function cloneFun(fn) {
const fnStr = fn.toString();
if (fn.prototype) {
// 有 prototype 的函数(构造函数)
return function() {
return fn.call(this, ...arguments);
};
} else {
// 箭头函数
return eval(fnStr);
}
}
// 防止循环引用版本
function deepClone(target, cache = new WeakMap()) {
// 单独开辟 cache,存储当前对象和拷贝对象的对应关系
if (cache.has(target)) {
return cache.get(target);
}
let copy;
if (typeof target === 'object' && target !== null) {
const type = Object.prototype.toString.call(target).match(/\s+(\w+)/)[1];
switch (type) {
case 'Function':
copy = cloneFun(target);
break;
case 'RegExp':
copy = new RegExp(target.source, target.flags);
break;
case 'Set':
copy = new Set();
break;
case 'Map':
copy = new Map();
break;
case 'Date':
copy = new Date(target);
break;
default:
copy = Array.isArray(target) ? [] : {};
}
// 将属性和拷贝后的值作为一个 map
cache.set(target, copy);
// 处理 Set
if (type === 'Set') {
target.forEach(value => {
copy.add(deepClone(value, cache));
});
return copy;
}
// 处理 Map
if (type === 'Map') {
target.forEach((value, key) => {
copy.set(key, deepClone(value, cache));
});
return copy;
}
// 处理普通对象和数组
if (copy) {
for (let key in target) {
if (target.hasOwnProperty(key)) {
let val = target[key];
copy[key] = deepClone(val, cache);
}
}
return copy;
}
}
// 基本类型直接返回
return target;
}
4.3 执行流程
graph TD
A[开始拷贝 target] --> B{是否在 cache 中?}
B -->|是| C[返回已拷贝的对象]
B -->|否| D{判断类型}
D -->|Function| E[克隆函数]
D -->|RegExp| F[创建新正则]
D -->|Date| G[创建新日期]
D -->|Set| H[创建新 Set]
D -->|Map| I[创建新 Map]
D -->|Array| J[创建新数组]
D -->|Object| K[创建新对象]
D -->|基本类型| L[直接返回]
E --> M[存入 cache]
F --> M
G --> M
H --> N[递归拷贝 Set 元素]
I --> O[递归拷贝 Map 键值]
J --> P[递归拷贝数组元素]
K --> Q[递归拷贝对象属性]
N --> M
O --> M
P --> M
Q --> M
M --> R[返回拷贝结果]
4.4 关键点解析
4.4.1 WeakMap 的作用
cache = new WeakMap();
为什么使用 WeakMap:
WeakMap的键必须是对象,值可以是任意类型WeakMap的键是弱引用,不会阻止垃圾回收- 适合存储对象之间的映射关系
4.4.2 循环引用检测
if (cache.has(target)) {
return cache.get(target);
}
原理:
- 在拷贝前检查对象是否已经在
cache中 - 如果存在,说明已经拷贝过,直接返回已拷贝的对象
- 避免了无限递归
4.4.3 函数克隆
function cloneFun(fn) {
const fnStr = fn.toString();
if (fn.prototype) {
// 普通函数
return function() {
return fn.call(this, ...arguments);
};
} else {
// 箭头函数
return eval(fnStr);
}
}
区别:
- 普通函数:有
prototype,需要保持this绑定 - 箭头函数:没有
prototype,可以直接eval字符串
4.5 测试用例
const obj = {
a: 1,
b: undefined,
c: {
name: '愚公上岸说',
},
d: [2],
fn: () => { return 2; },
map: new Map([[1, 2]])
};
obj.obj = obj; // 循环引用
const copy = deepClone(obj);
obj.c.name = 2;
console.log('obj---', obj);
// obj--- <ref *1> {
// a: 1,
// b: undefined,
// c: { name: 2 },
// d: [ 2 ],
// fn: [Function: fn],
// map: Map(1) { 1 => 2 },
// obj: [Circular *1]
// }
console.log('copy--', copy);
// copy-- <ref *1> {
// a: 1,
// b: undefined,
// c: { name: '愚公上岸说' },
// d: [ 2 ],
// fn: [Function: fn],
// map: Map(1) { 1 => 2 },
// obj: [Circular *1]
// }
验证结果:
- ✅ 循环引用问题已解决
- ✅ 修改原对象不影响拷贝对象
- ✅ 所有类型都能正确拷贝
五、优化版本
5.1 支持更多类型
function deepClone(target, cache = new WeakMap()) {
if (cache.has(target)) {
return cache.get(target);
}
let copy;
if (typeof target === 'object' && target !== null) {
const type = Object.prototype.toString.call(target).match(/\s+(\w+)/)[1];
switch (type) {
case 'Function':
copy = cloneFun(target);
break;
case 'RegExp':
copy = new RegExp(target.source, target.flags);
break;
case 'Set':
copy = new Set();
break;
case 'Map':
copy = new Map();
break;
case 'Date':
copy = new Date(target);
break;
case 'Error':
copy = new Error(target.message);
break;
case 'ArrayBuffer':
copy = target.slice();
break;
default:
copy = Array.isArray(target) ? [] : {};
}
cache.set(target, copy);
// 处理 Set
if (type === 'Set') {
target.forEach(value => {
copy.add(deepClone(value, cache));
});
return copy;
}
// 处理 Map
if (type === 'Map') {
target.forEach((value, key) => {
copy.set(deepClone(key, cache), deepClone(value, cache));
});
return copy;
}
// 处理普通对象和数组
if (copy) {
for (let key in target) {
if (target.hasOwnProperty(key)) {
copy[key] = deepClone(target[key], cache);
}
}
return copy;
}
}
return target;
}
5.2 性能优化
function deepClone(target, cache = new WeakMap()) {
// 基本类型直接返回
if (target === null || typeof target !== 'object') {
return target;
}
// 检查缓存
if (cache.has(target)) {
return cache.get(target);
}
// ... 其余代码
}
六、方法对比
| 方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| JSON.stringify | 实现简单 | 不支持函数、Date、RegExp、循环引用 | 简单对象 |
| 简单递归 | 支持更多类型 | 不支持循环引用 | 无循环引用场景 |
| 终极版 | 功能完整 | 实现复杂 | 生产环境 |
七、面试要点总结
核心知识点
- 深拷贝 vs 浅拷贝:深拷贝递归复制所有层级
- 循环引用处理:使用
WeakMap存储已拷贝对象 - 类型处理:需要特殊处理函数、Date、RegExp、Set、Map 等
- 性能优化:缓存已拷贝对象,避免重复拷贝
常见面试题
Q1: 如何实现深拷贝?
答:递归遍历对象,对每个属性进行拷贝。需要处理特殊类型(函数、Date、RegExp 等),并使用 WeakMap 解决循环引用问题。
Q2: 如何解决循环引用?
答:使用 WeakMap 存储已拷贝的对象,在拷贝前检查对象是否已存在,如果存在直接返回,避免无限递归。
Q3: JSON.stringify 实现深拷贝有什么问题?
答:
- 不支持函数、
undefined、Symbol - 不能正确处理
Date、RegExp、Set、Map - 循环引用会导致栈溢出
实战建议
- ✅ 生产环境可以使用
lodash.cloneDeep - ✅ 理解深拷贝的原理有助于解决实际问题
- ✅ 注意循环引用和特殊类型的处理
- ✅ 考虑性能优化,避免不必要的拷贝
前端面试小册 文章被收录于专栏
每天更新3-4节,持续更新中... 目标:50天学完,上岸银行总行!


