8 实现深拷贝

1 简洁版本

简单版:

    const newObj = JSON.parse(JSON.stringify(oldObj));

局限性:

  • 他无法实现对函数 、RegExp等特殊对象的克隆
  • 会抛弃对象的constructor,所有的构造函数会指向Object
  • 对象有循环引用,会报错

面试简版

    function deepClone(obj) {
      // 如果是 值类型 或 null,则直接return
      if(typeof obj !== 'object' || obj === null) {
        return obj
      }
      
      // 定义结果对象
      let copy = {}
      
      // 如果对象是数组,则定义结果数组
      if(obj instanceof Array) {
        copy = []
      }
      
      // 遍历对象的key
      for(let key in obj) {
          // 如果key是对象的自有属性
          if(obj.hasOwnProperty(key)) {
            // 递归调用深拷贝方法
            copy[key] = deepClone(obj[key])
          }
      }
      
      return copy
    } 

调用深拷贝方法,若属性为值类型,则直接返回;若属性为引用类型,则递归遍历。这就是我们在解这一类题时的核心的方法。

进阶版

  • 解决拷贝循环引用问题
  • 解决拷贝对应原型问题
    // 递归拷贝 (类型判断)
    function deepClone(value,hash = new WeakMap){ // 弱引用,不用map,weakMap更合适一点
      // null 和 undefiend 是不需要拷贝的
      if(value == null){ return value;}
      if(value instanceof RegExp) { return new RegExp(value) }
      if(value instanceof Date) { return new Date(value) }
      // 函数是不需要拷贝
      if(typeof value != 'object') return value;
      let obj = new value.constructor(); // [] {}
      // 说明是一个对象类型
      if(hash.get(value)){
        return hash.get(value)
      }
      hash.set(value,obj);
      for(let key in value){ // in 会遍历当前对象上的属性 和 __proto__指代的属性
        // 补拷贝 对象的__proto__上的属性
        if(value.hasOwnProperty(key)){
          // 如果值还有可能是对象 就继续拷贝
          obj[key] = deepClone(value[key],hash);
        }
      }
      return obj
      // 区分对象和数组 Object.prototype.toString.call
    }
    // test
    
    var o = {};
    o.x = o;
    var o1 = deepClone(o); // 如果这个对象拷贝过了 就返回那个拷贝的结果就可以了
    console.log(o1);

2 实现完整的深拷贝

1. 简易版及问题

    JSON.parse(JSON.stringify());

估计这个api能覆盖大多数的应用场景,没错,谈到深拷贝,我第一个想到的也是它。但是实际上,对于某些严格的场景来说,这个方法是有巨大的坑的。问题如下:

  1. 无法解决循环引用的问题。举个例子:
    const a = {val:2};
    a.target = a;

拷贝a会出现系统栈溢出,因为出现了无限递归的情况。

  1. 无法拷贝一些特殊的对象,诸如 RegExp, Date, Set, Map
  2. 无法拷贝函数(划重点)。

因此这个api先pass掉,我们重新写一个深拷贝,简易版如下:

    const deepClone = (target) => {
      if (typeof target === 'object' && target !== null) {
        const cloneTarget = Array.isArray(target) ? []: {};
        for (let prop in target) {
          if (target.hasOwnProperty(prop)) {
              cloneTarget[prop] = deepClone(target[prop]);
          }
        }
        return cloneTarget;
      } else {
        return target;
      }
    }

现在,我们以刚刚发现的三个问题为导向,一步步来完善、优化我们的深拷贝代码。

2. 解决循环引用

现在问题如下:

    let obj = {val : 100};
    obj.target = obj;
    
    deepClone(obj);//报错: RangeError: Maximum call stack size exceeded

这就是循环引用。我们怎么来解决这个问题呢?

创建一个Map。记录下已经拷贝过的对象,如果说已经拷贝过,那直接返回它行了。

    const isObject = (target) => (typeof target === 'object' || typeof target === 'function') && target !== null;
    
    const deepClone = (target, map = new Map()) => { 
      if(map.get(target))  
        return target; 
     
     
      if (isObject(target)) { 
        map.set(target, true); 
        const cloneTarget = Array.isArray(target) ? []: {}; 
        for (let prop in target) { 
          if (target.hasOwnProperty(prop)) { 
              cloneTarget[prop] = deepClone(target[prop],map); 
          } 
        } 
        return cloneTarget; 
      } else { 
        return target; 
      } 
    }

现在来试一试:

    const a = {val:2};
    a.target = a;
    let newA = deepClone(a);
    console.log(newA)//{ val: 2, target: { val: 2, target: [Circular] } }

好像是没有问题了, 拷贝也完成了。但还是有一个潜在的坑, 就是map 上的 key 和 map 构成了强引用关系,这是相当危险的。我给你解释一下与之相对的弱引用的概念你就明白了

在计算机程序设计中,弱引用与强引用相对,

被弱引用的对象可以在任何时候被回收,而对于强引用来说,只要这个强引用还在,那么对象无法被回收。拿上面的例子说,map 和 a一直是强引用的关系, 在程序结束之前,a 所占的内存空间一直不会被释放。

怎么解决这个问题?

很简单,让 map 的 key 和 map 构成弱引用即可。ES6给我们提供了这样的数据结构,它的名字叫WeakMap,它是一种特殊的Map, 其中的键是弱引用的。其键必须是对象,而值可以是任意的

稍微改造一下即可:

    const deepClone = (target, map = new WeakMap()) => {
      //...
    }

3. 拷贝特殊对象

可继续遍历

对于特殊的对象,我们使用以下方式来鉴别:

    Object.prototype.toString.call(obj);

梳理一下对于可遍历对象会有什么结果:

    ["object Map"]
    ["object Set"]
    ["object Array"]
    ["object Object"]
    ["object Arguments"]

以这些不同的字符串为依据,我们就可以成功地鉴别这些对象。

    const getType = Object.prototype.toString.call(obj);
    
    const canTraverse = {
      '[object Map]': true,
      '[object Set]': true,
      '[object Array]': true,
      '[object Object]': true,
      '[object Arguments]': true,
    };
    
    const deepClone = (target, map = new Map()) => {
      if(!isObject(target)) 
        return target;
      let type = getType(target);
      let cloneTarget;
      if(!canTraverse[type]) {
        // 处理不能遍历的对象
        return;
      }else {
        // 这波操作相当关键,可以保证对象的原型不丢失!
        let ctor = target.prototype;
        cloneTarget = new ctor();
      }
    
      if(map.get(target)) 
        return target;
      map.put(target, true);
    
      if(type === mapTag) {
        //处理Map
        target.forEach((item, key) => {
          cloneTarget.set(deepClone(key), deepClone(item));
        })
      }
      
      if(type === setTag) {
        //处理Set
        target.forEach(item => {
          target.add(deepClone(item));
        })
      }
    
      // 处理数组和对象
      for (let prop in target) {
        if (target.hasOwnProperty(prop)) {
            cloneTarget[prop] = deepClone(target[prop]);
        }
      }
      return cloneTarget;
    }

不可遍历的对象

    const boolTag = '[object Boolean]';
    const numberTag = '[object Number]';
    const stringTag = '[object String]';
    const dateTag = '[object Date]';
    const errorTag = '[object Error]';
    const regexpTag = '[object RegExp]';
    const funcTag = '[object Function]';

对于不可遍历的对象,不同的对象有不同的处理。

    const handleRegExp = (target) => {
      const { source, flags } = target;
      return new target.constructor(source, flags);
    }
    
    const handleFunc = (target) => {
      // 待会的重点部分
    }
    
    const handleNotTraverse = (target, tag) => {
      const Ctor = targe.constructor;
      switch(tag) {
        case boolTag:
        case numberTag:
        case stringTag:
        case errorTag: 
        case dateTag:
          return new Ctor(target);
        case regexpTag:
          return handleRegExp(target);
        case funcTag:
          return handleFunc(target);
        default:
          return new Ctor(target);
      }
    }

4. 拷贝函数

  • 虽然函数也是对象,但是它过于特殊,我们单独把它拿出来拆解。
  • 提到函数,在JS种有两种函数,一种是普通函数,另一种是箭头函数。每个普通函数都是
  • Function的实例,而箭头函数不是任何类的实例,每次调用都是不一样的引用。那我们只需要
  • 处理普通函数的情况,箭头函数直接返回它本身就好了。

那么如何来区分两者呢?

答案是: 利用原型。箭头函数是不存在原型的。

    const handleFunc = (func) => {
      // 箭头函数直接返回自身
      if(!func.prototype) return func;
      const bodyReg = /(?<={)(.|\n)+(?=})/m;
      const paramReg = /(?<=\().+(?=\)\s+{)/;
      const funcString = func.toString();
      // 分别匹配 函数参数 和 函数体
      const param = paramReg.exec(funcString);
      const body = bodyReg.exec(funcString);
      if(!body) return null;
      if (param) {
        const paramArr = param[0].split(',');
        return new Function(...paramArr, body[0]);
      } else {
        return new Function(body[0]);
      }
    }

5. 完整代码展示

    const getType = obj => Object.prototype.toString.call(obj);
    
    const isObject = (target) => (typeof target === 'object' || typeof target === 'function') && target !== null;
    
    const canTraverse = {
      '[object Map]': true,
      '[object Set]': true,
      '[object Array]': true,
      '[object Object]': true,
      '[object Arguments]': true,
    };
    const mapTag = '[object Map]';
    const setTag = '[object Set]';
    const boolTag = '[object Boolean]';
    const numberTag = '[object Number]';
    const stringTag = '[object String]';
    const symbolTag = '[object Symbol]';
    const dateTag = '[object Date]';
    const errorTag = '[object Error]';
    const regexpTag = '[object RegExp]';
    const funcTag = '[object Function]';
    
    const handleRegExp = (target) => {
      const { source, flags } = target;
      return new target.constructor(source, flags);
    }
    
    const handleFunc = (func) => {
      // 箭头函数直接返回自身
      if(!func.prototype) return func;
      const bodyReg = /(?<={)(.|\n)+(?=})/m;
      const paramReg = /(?<=\().+(?=\)\s+{)/;
      const funcString = func.toString();
      // 分别匹配 函数参数 和 函数体
      const param = paramReg.exec(funcString);
      const body = bodyReg.exec(funcString);
      if(!body) return null;
      if (param) {
        const paramArr = param[0].split(',');
        return new Function(...paramArr, body[0]);
      } else {
        return new Function(body[0]);
      }
    }
    
    const handleNotTraverse = (target, tag) => {
      const Ctor = target.constructor;
      switch(tag) {
        case boolTag:
          return new Object(Boolean.prototype.valueOf.call(target));
        case numberTag:
          return new Object(Number.prototype.valueOf.call(target));
        case stringTag:
          return new Object(String.prototype.valueOf.call(target));
        case symbolTag:
          return new Object(Symbol.prototype.valueOf.call(target));
        case errorTag: 
        case dateTag:
          return new Ctor(target);
        case regexpTag:
          return handleRegExp(target);
        case funcTag:
          return handleFunc(target);
        default:
          return new Ctor(target);
      }
    }
    
    const deepClone = (target, map = new WeakMap()) => {
      if(!isObject(target)) 
        return target;
      let type = getType(target);
      let cloneTarget;
      if(!canTraverse[type]) {
        // 处理不能遍历的对象
        return handleNotTraverse(target, type);
      }else {
        // 这波操作相当关键,可以保证对象的原型不丢失!
        let ctor = target.constructor;
        cloneTarget = new ctor();
      }
    
      if(map.get(target)) 
        return target;
      map.set(target, true);
    
      if(type === mapTag) {
        //处理Map
        target.forEach((item, key) => {
          cloneTarget.set(deepClone(key, map), deepClone(item, map));
        })
      }
      
      if(type === setTag) {
        //处理Set
        target.forEach(item => {
          cloneTarget.add(deepClone(item, map));
        })
      }
    
      // 处理数组和对象
      for (let prop in target) {
        if (target.hasOwnProperty(prop)) {
            cloneTarget[prop] = deepClone(target[prop], map);
        }
      }
      return cloneTarget;
    }

补充(现代做法):浏览器与 Node 17+ 已内置 structuredClone(obj),原生支持循环引用、Date、RegExp、Map、Set、ArrayBuffer 等,深拷贝纯数据时优先用它。局限是不能拷贝函数、DOM 节点、Symbol 键、原型链(遇到会抛错或丢失),这些场景仍需手写递归。JSON.parse(JSON.stringify(obj)) 是最省事的写法,但会丢失 undefined/函数/Symbol,且把 Date 变成字符串、无法处理循环引用——面试时要能说清它的这些坑。

Last Updated:
Contributors: leeguooooo