偶然发现自己欠了一篇文章,那么今天就来自己动手实现一个深拷贝
之前的文章(《赋值、浅拷贝与深拷贝》)我们讲过深浅拷贝的概念、区别以及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]'||Object.prototype.toString.call(obj) ==='[object Array]' } 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();复制代码
查看控制台输出结果:
可见第三版方法对于对象的循环引用也可以完美拷贝了!
如果有错误或者不严谨的地方,请给予指正,十分感谢!