如何用递归实现深拷贝?

239 阅读6分钟

浅拷贝的几种方式

const obj = [1,2,3,4,{a:1,b:2}]
const obj2 = [...obj]
const obj3 = Object.assign([],obj)
const obj4 = obj.slice(0)
const obj5 = obj.concat()

为什么需要用到深拷贝?

const obj = [1,2,3,4,{a:1,b:2}]
// 展开运算符浅拷贝
const obj2 = [...obj]
// 改变obj2里面对象的a属性,希望只改变obj2,不要影响到obj
obj2[4].a = 10000 
console.log(obj2) // obj2改变了
console.log(obj) // obj1也改变了

image.png

原因分析

  • 在浅拷贝时,复制对象的时候并没有创造一个新的对象
  • 只是复制了对象地址
  • 所以在改变同一个地址的对象时,obj和obj2的内容就都变了

如何改变

  • 至少在深拷贝对象的时候
  • 需要在内存中找到一片全新的空间
  • 然后把原来的内容放到这片空间里
  • 这样原来的对象跟深拷贝后的对象就属于不同地址了
  • 这样就不会互相影响了

深拷贝有哪些要点?

  • 判断要拷贝的类型

    • 对象
      • 对象构造出来的类型
        • Date
        • RegExp
        • Function
          • 箭头函数(没有prototype属性)
          • 普通函数
        • Array
      • 普通对象
    • 其他基本类型
  • 递归拷贝子属性

    • 只拷贝自身的属性,不拷贝原型上的属性
      • 用到hasOwnProperty
  • 解决循环引用问题

    • 记住哪些已经拷贝过了
    • 用Map记录
      • Map用对象存储键值对
        • Map的key可以是对象

如何深拷贝函数?

判断类型

  • 是否为对象
  • 是否为函数
  • 是普通函数还是箭头函数
const cloneFn = function(p) {
    // 判断参数p是否为Object的实例
  if (p instanceof Object) {
    // 声明result,用来存放拷贝的结果
    let result;
    // 判断p是否为Function的实例
    if (p instanceof Function) {
      // 判断p是否为普通函数
      if (p.prototype) {
        result = function () {
          // 让拷贝的结果等于一个函数,然后这个函数返回p本身
          // 绑定this,然后传入实参
          return p.apply(this, arguments);
        };
      } else {
        // 当不是普通函数时,让result等于箭头函数
        result = (...args) => {
          // 注意箭头函数没有this,所以设置为空
          return p.call(null, ...args);
        };
      }
    } 
    // 遍历参数p的子属性,递归实现深拷贝
    for (let key in p) {
      // 这一步是为了不拷贝原型上的属性
      // 判断p自身属性上是否有这个属性,自身有才拷贝
      if (p.hasOwnProperty(key)) {
        // 将键值深拷贝
        // 让result创造对应key的紫属性
        // 把拷贝的键值,放入对应的result子属性中
        result[key] = cloneFn(p[key]);
      }
    }
    // 返回拷贝后的结果
    return result;
  }
}

函数的深拷贝测试

image.png

如何拷贝Object构造出来的类型

有哪些常见的用Object构造出来的类型?

  • Date

  • RegExp

  • 函数

    • 箭头函数(没有prototype属性)
    • 普通函数(有prototype属性)
  • 普通对象

实现

const deepClone = function (p) {
  // 判断参数p是否为Object的实例
  if (p instanceof Object) {
    // 声明result,用来存放拷贝的结果
    let result;
    // 判断p是否为Function的实例
    if (p instanceof Function) {
      // 判断p是否为普通函数
      if (p.prototype) {
        result = function () {
          // 让拷贝的结果等于一个函数,然后这个函数返回p本身
          // 绑定this,然后传入实参
          return p.apply(this, arguments);
        };
      } else {
        // 当不是普通函数时,让result等于箭头函数
        result = (...args) => {
          // 注意箭头函数没有this,所以设置为空
          return p.call(null, ...args);
        };
      }
    } else if (p instanceof Array) {
      result = [];
    } else if (p instanceof Date) { //判断是否为时间
      // 把时间转化成数字(时间戳)
      result = new Date(+p); 
    } else if (p instanceof RegExp) { // 判断是否为正则
      //返回一个新的正则
      result = new RegExp(p.source, p.flags);
    } else {
        // 其他的情况,返回一个普通的对象
        return {}
    }
    // 遍历参数p的子属性,递归实现深拷贝
    for (let key in p) {
      // 这一步是为了不拷贝原型上的属性
      // 判断p自身属性上是否有这个属性,自身有才拷贝
      if (p.hasOwnProperty(key)) {
        // 将键值深拷贝
        // 让result创造对应key的紫属性
        // 把拷贝的键值,放入对应的result子属性中
        result[key] = deepClone(p[key]);
      }
    }
    // 返回拷贝后的结果
    return result;
  }
};

实现其他基本类型的深拷贝

  • 其他的基础类型,等于其自身就可以了
const deepClone = function (p) {
  if (p instanceof Object) {
    let result;
    if (p instanceof Function) {
      if (p.prototype) {
        result = function () {
          return p.apply(this, arguments);
        };
      } else {
        result = (...args) => {
          return p.call(null, ...args);
        };
      }
    } else if (p instanceof Array) {
      result = [];
    } else if (p instanceof Date) {
      result = new Date(+p);
    } else if (p instanceof RegExp) {
      result = new RegExp(p.source, p.flags);
    } else {
        return {}
    }
    
    for (let key in p) {
      if (p.hasOwnProperty(key)) {
        result[key] = deepClone(p[key]);
      }
    }
    return result;
  } else {
    // 其他的基础类型,等于其自身就可以了
    result = p;
    return result
  }
};

目前这样做的缺点是什么?

  • 当要复制的参数p,它的子属性是其自身时,会出现调用栈溢出的问题
  • a.self = a
  • 也就是循环引用的问题

image.png

  • 为什么会出现问题

什么是循环引用,用图表示下

  • 对象的属性直接的引用了自身的情况

  • 如果a对象有个self属性指向其自身

  • image.png

怎么解决循环引用的问题

  • 额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系

    • Map的键名可以是对象
    • image.png
  • 当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象

    • 如果发现要拷贝的内容,已经拷贝过了,直接返回
    • 如果没有的话继续拷贝

image.png

const deepClone = (a, cache) => {
  if (!cache) {
    // 这里千万不要用let声明,用了的话cache就只在这个if语句中生效了
    cache = new Map(); // 临时创建并递归传递
  }
  if (a instanceof Object) {
    // 如果要拷贝的内容,已经拷贝过了,直接返回
    if (cache.get(a)) return cache.get(a);
    let result;
    if (a instanceof Function) {
      if (a.prototype) {
        // 有 prototype 就是普通函数
        result = function () {
          return a.apply(this, arguments);
        };
      } else {
        result = (...args) => {
          return a.call(undefined, ...args);
        };
      }
    } else if (a instanceof Array) {
      result = [];
    } else if (a instanceof Date) {
      result = new Date(a - 0);
    } else if (a instanceof RegExp) {
      result = new RegExp(a.source, a.flags);
    } else {
      result = {};
    }
    // 如果没有拷贝过这个对象,那么就将当前对象作为key,克隆对象作为value进行存储
    cache.set(a, result);
    // 遍历参数p,递归深克隆其自身属性
    for (let key in a) {
      // 判断key是否在其自身上
      if (a.hasOwnProperty(key)) {
        result[key] = deepClone(a[key], cache); // 递归传递,注意这时把cache也传过去了
      }
    }
    return result;
  } else {
    return a;
  }
};

代码测试

const a = {
  number: 1,
  bool: false,
  str: "hi",
  empty1: undefined,
  empty2: null,
  array: [
    { name: "frank", age: 18 },
    { name: "jacky", age: 19 },
  ],
  date: new Date(2000, 0, 1, 20, 30, 0),
  regex: /.(j|t)sx/i,
  obj: { name: "frank", age: 18 },
  f1: (a, b) => a + b,
  f2: function (a, b) {
    return a + b;
  },
};

// 循环引用测试
a.self = a
const b = deepClone(a)
b.self === b // true
b.self = 'hi'
a.self !== 'hi' //true

// 函数深拷贝测试
const f1 = function(e) {
    console.log('hello')
    console.log(e)
}
const f2 = deepClone(f1)
f2(3) // 'hello' 3