手写深拷贝

偶然发现自己欠了一篇文章,那么今天就来自己动手实现一个深拷贝

之前的文章(《赋值、浅拷贝与深拷贝》)我们讲过深浅拷贝的概念、区别以及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();复制代码

查看控制台输出结果:


可见第三版方法对于对象的循环引用也可以完美拷贝了!

如果有错误或者不严谨的地方,请给予指正,十分感谢!


转载于:https://juejin.im/post/5cc6e005e51d456e62545b70

你可能感兴趣的:(手写深拷贝)