前言
网上关于深浅拷贝的定义有很多,以下是摘自ChatGPT的回答:
浅拷贝 : 浅拷贝是指创建一个新的对象,新对象的属性值和原始对象的属性值相同,但是对于属性值为对象的情况,新对象仍然引用这些对象的地址。换句话说,浅拷贝只是复制了对象的第一层结构,而没有递归地复制更深层的对象。
举个例子,如果有一个对象 A,其中包含一个属性 B,而 B 又是一个对象。那么进行浅拷贝后的对象如果修改了 B 对象中的属性,原始对象和浅拷贝对象都会受到影响。
深拷贝 : 深拷贝是指创建一个新的对象,新对象的所有属性值和原始对象的属性值完全相同,包括所有层级的嵌套对象。换句话说,深拷贝会递归地复制对象的所有层级,确保新对象和原始对象完全独立,互不影响。
浅拷贝
JavaScript实现浅拷贝的方法有很多,以下列举几个比较常见的:
- Object.assign()
- 扩展运算符
- 数组实例上的若干方法(map, filter, slice, concat)等
深拷贝
JSON.stringify, JSON.parse
最简单的方法是 JSON.parse(JSON.stringify(obj))
,它的使用方法如下:
js
const obj = {
a: 1,
b: 2
}
const cloneObj = JSON.parse(JSON.stringify(obj));
cloneObj.a = 2;
console.log(obj); // { a: 1, b: 2 }
但是,这种方法有缺点:
- 会忽略
undefined
、Symbol
和函数
NaN
、Infinity
会被序列化成null
- 原型链丢失
- 无法处理循环引用问题
这种方法可以用于日常开发中深拷贝简单对象的场景。接下来,我们试着实现一个深拷贝,并处理各种边界情况
第一层拷贝
先实现第一层拷贝
js
const clone = (target) => {
// 定义一个拷贝对象
const cloneTarget = {};
for(const key in target) {
cloneTarget[key] = target[key];
}
return cloneTarget;
}
克隆后,原对象更深层的对象会共享同一地址
js
const obj = {
a: 1,
b: {
c: 2,
d: 3
},
e: 4
};
const newObj = clone(obj);
newObj.b.c = 444;
console.log(obj); // {a: 1, b: {c: 444, d: 3}, e: 4}
console.log(newObj) // {a: 1, b: {c: 444, d: 3}, e: 4}
使用递归
在上面的基础上加入递归,处理深层对象没有拷贝的问题
js
const deepClone = (target) => {
// null或基本数据类型则直接返回
if(target === null || typeof target !== "object") {
return target;
}
// 定义一个拷贝对象
const cloneTarget = {};
for(const key in target) {
// 递归拷贝
cloneTarget[key] = deepClone(target[key]);
}
return cloneTarget;
}
测试:
js
const obj = {
a: 1,
b: {
c: 2,
d: 3
},
e: 4
};
const newObj = clone(obj);
newObj.b.c = 444;
console.log(obj); // {a: 1, b: {c: 2, d: 3}, e: 4}
console.log(newObj) // {a: 1, b: {c: 444, d: 3}, e: 4}
深层对象也不共享内存地址了
处理其它引用类型
递归可以解决拷贝目标为普通对象的拷贝问题,但如果拷贝目标是Array、Date、RegExp这些非普通对象,就需要进行特殊处理
js
const deepClone = (target) => {
// null或基本数据类型则直接返回
if(target === null || typeof target !== "object") {
return target;
}
// 若拷贝对象诸如Date, RegExp这些特殊引用类型,则需要创建新的实例返回
if(target instanceof Date) return new Date(Date);
if(target instanceof RegExp) return new RegExp(target);
// 其它引用类型处理...
// 创建拷贝对象
const cloneTarget = Array.isArray(target) ? [] : {};
for(const key in target) {
// 递归拷贝
cloneTarget[key] = deepClone(target[key]);
}
return cloneTarget;
}
上述代码演示了对Date, Regexp和Arrary三种特殊类型的处理,但Js内置的引用类型比较多,可以参考Loadsh深拷贝函数源码对这些类型的定义:
处理Symbol类型
for...in
循环无法遍历出使用Symbol键值,需要改成Reflect.ownKey()
:
js
const deepClone = (target) => {
// null或基本数据类型则直接返回
if(target === null || typeof target !== "object") {
return target;
}
// 若拷贝对象诸如Date, RegExp这些特殊引用类型,则需要创建新的实例返回
if(target instanceof Date) return new Date(Date);
if(target instanceof RegExp) return new RegExp(target);
// 其它引用类型处理...
// 创建拷贝对象
const cloneTarget = Array.isArray(target) ? [] : {};
// 这里等价于: Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))
Reflect.ownKeys(target).forEach(key => {
// 递归拷贝
cloneTarget[key] = deepClone(target[key]);
})
return cloneTarget;
}
测试:
js
const obj = {a: 1};
const key = Symbol("b");
obj[key] = 2;
const newObj = deepClone(obj);
console.log(newObj); // {a: 1, Symbol(b): 2}
成功拷贝了Symbol键
处理循环引用
若对象中的某一个键的值就是这个对象本身,就会形成循环引用:
js
const obj = {
a: 1,
b: 2,
c: 3
}
obj.child = obj;
这时使用上面的深拷贝函数,会因为无限递归进入死循环
解决方法是额外开辟一个内存空间存储当前对象和拷贝对象的关系,当需要拷贝当前对象时,先去存储空间中找,若存在过则直接返回,这样就避免了无限递归的场景:
js
// 新加一个cacheTarget用于存储当前对象和拷贝对象间的关系
const deepClone = (target, cacheMap = new WeakMap()) => {
// null或基本数据类型则直接返回
if(target === null || typeof target !== "object") {
return target;
}
// 若拷贝对象诸如Date, RegExp这些特殊引用类型,则需要创建新的实例返回
if(target instanceof Date) return new Date(Date);
if(target instanceof RegExp) return new RegExp(target);
// 其它引用类型处理...
// 创建拷贝对象前先判断当前对象是否被拷贝过,若存在则直接方法
if(cacheMap.has(target)) return cacheMap.get(target)
// 创建拷贝对象
const cloneTarget = Array.isArray(target) ? [] : {};
// 存储当前对象和拷贝对象进cacheTarget
cacheMap.set(target, cloneTarget);
// 这里等价于: Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))
Reflect.ownKeys(target).forEach(key => {
// 递归拷贝, 这里别忘了传入cacheMap
cloneTarget[key] = deepClone(target[key], cacheMap);
})
return cloneTarget;
}
测试:
js
const obj = {
a: 1,
b: 2,
c: 3
}
obj.child = obj;
const newObj = deepClone(obj);
console.log(newObj) // {a: 1, b: 2, c: 3, child: {...}}
总结
解决完循环递归,基本已将深拷贝的大部分场景都考虑进去,当然是还有很多缺陷,如对函数的处理。
面试中如果遇到手写深拷贝的场景,能写到这里绝对够了。如果是实际的开发场景,那还需要考虑性能,团队规范等实际情况,这里便不再赘述。
如果文章对您有帮助,您的👍便是对博主最大的鼓励,也欢迎在评论区留下您宝贵的意见