偶然发现自己欠了一篇文章,那么今天就来自己动手实现一个深拷贝
之前的文章(《赋值、浅拷贝与深拷贝》)我们讲过深浅拷贝的概念、区别以及JSON.parse(JSON.stringify(obj))实现深拷贝存在的问题:
对于undefined,函数,Symbol会直接忽略
new Date()转换后结果不正确
对于正则转换为{}
对于循环引用,会报错
说到底,想要实现深拷贝,就是浅拷贝加递归,也就是如果对象的属性值还是一个对象的话,再进行一次拷贝,话不多说,上代码:
首先定义一个供深拷贝使用的对象:
let obj1 = {
name:'obj.name',
un:undefined,
nu:null,
sy:Symbol(123),
say:function(){
console.log(this.name);
},
reg:/\d{6}/g,
date:new Date(),
child:{
name:'child.name'
}
}复制代码
可见如上对象的属性值包含了JSON.parse(JSON.stringify(obj))存在问题的所有数据类型,接下来让我们实现一个深拷贝并一一解决JSON.parse(JSON.stringify(obj))深拷贝存在的问题
首先我们讲,实现深拷贝,就是遍历对象的key,并将value赋给新的对象的key,如果原对象的属性值为对象,则递归调用深拷贝方法(这里指的属性值为对象指有自己属性的对象,区别于正则,Date对象等),于是有了如下第一版代码:
function isObject(obj) {
return typeof obj === 'object' && obj != null;
}
function deepCopy(source){
// 判断如果参数不是一个对象,返回改参数
if(!isObject(source)) return source;
// 判断参数是对象还是数组来初始化返回值
let res = Array.isArray(source)?[]:{};
// 循环参数对象的key
for(let key in source){
// 如果该key属于参数对象本身
if(Object.prototype.hasOwnProperty.call(source,key)){
// 如果该key的value值是对象,递归调用深拷贝方法进行拷贝
if(isObject(source[key])){
res[key] = deepCopy(source[key]);
}else{
// 如果该key的value值不是对象,则把参数对象key的value值赋给返回值的key
res[key] = source[key];
}
}
}
// 返回返回值
return res;
};复制代码
然后用如下代码来比对该方法的成果:
let obj2 = deepCopy(obj1);
console.log(obj1);
console.log(obj2);
console.log(obj2.sy === obj1.sy)
obj2.name = 'obj2.name';
obj2.say();复制代码
查看控制台输出结果:
可见第一版方法对于Date,正则的拷贝变成了空对象,对于方法及Symbol的拷贝都是没有问题的,其实对于第一版方法中判断source[key]是否是对象的方法isObject,对于Date对象和正则也会返回true,而这两种对象再次递归调用深拷贝方法的时候,由于其没有可遍历的key,所以返回的就是初始化的{},找到了问题点,我们优化上面的方法如下:
function isObject(obj) {
return Object.prototype.toString.call(obj) === '[object Object]'
}
function deepCopy(source){
// 判断如果参数不是一个对象,返回改参数
if(!isObject(source)) return source;
// 判断参数是对象还是数组来初始化返回值
let res = Array.isArray(source)?[]:{};
// 循环参数对象的key
for(let key in source){
// 如果该key属于参数对象本身
if(Object.prototype.hasOwnProperty.call(source,key)){
// 如果该key的value值是对象,递归调用深拷贝方法进行拷贝
if(isObject(source[key])){
res[key] = deepCopy(source[key]);
}else{
// 如果该key的value值不是对象,则把参数对象key的value值赋给返回值的key
res[key] = source[key];
}
}
}
// 返回返回值
return res;
};复制代码
再次用如下代码来比对该方法的成果:
let obj2 = deepCopy(obj1);
console.log(obj1);
console.log(obj2);
console.log(obj2.sy === obj1.sy)
obj2.name = 'obj2.name';
obj2.say();复制代码
查看控制台输出结果:
可见第二版的方法对于Date和正则的拷贝已经完全没有问题了,那么我们再处理最后一个问题:循环引用
调用深拷贝之前添加如下代码:
obj1.child.child= obj1.child;
复制代码
再次用如下代码来比对该方法的成果:
let obj2 = deepCopy(obj1);
console.log(obj1);
console.log(obj2);
console.log(obj2.sy === obj1.sy)
obj2.name = 'obj2.name';
obj2.say();复制代码
查看控制台输出结果:
会发现第二版的方法对于循环引用的对象不停地递归调用,然后就爆栈了
解决如上方法,只需要在递归深拷贝之前判断是否已经拷贝过该对象,是的话把该对象返回,不要再递归下去即可,采用ES6的WeakMap对象保存已经拷贝过的对象,修改deepCopy方法如下:
function deepCopy(source,hash = new WeakMap()){
// 判断如果参数不是一个对象,返回改参数
if(!isObject(source)) return source;
if(hash.has(source)) return hash.get(source); // 如何拷贝过该对象,则直接返回该对象
// 判断参数是对象还是数组来初始化返回值
let res = Array.isArray(source)?[]:{};
hash.set(source,res); // 哈希表添加新对象
// 循环参数对象的key
for(let key in source){
// 如果该key属于参数对象本身
if(Object.prototype.hasOwnProperty.call(source,key)){
// 如果该key的value值是对象,递归调用深拷贝方法进行拷贝
if(isObject(source[key])){
res[key] = deepCopy(source[key],hash);
}else{
// 如果该key的value值不是对象,则把参数对象key的value值赋给返回值的key
res[key] = source[key];
}
}
}
// 返回返回值
return res;
};
复制代码
再次用如下代码来比对该方法的成果:
let obj2 = deepCopy(obj1);
console.log(obj1);
console.log(obj2);
console.log(obj2.sy === obj1.sy)
obj2.name = 'obj2.name';
obj2.say();复制代码
查看控制台输出结果:
可见第三版方法对于对象的循环引用也可以完美拷贝了!
如果有错误或者不严谨的地方,请给予指正,十分感谢!