美文网首页
不积跬步之手写深拷贝

不积跬步之手写深拷贝

作者: 雨飞飞雨 | 来源:发表于2022-03-29 19:48 被阅读0次
    深拷贝与浅拷贝.png

    浅拷贝和深拷贝的区别

    大家都以为浅拷贝就是把引用类型的值拷贝一份,实际还是引用的同一个对象,把这个叫浅拷贝,实际上这是一个大大的误会。

    • 浅拷贝和深拷贝都复制了值和地址,都是为了解决引用类型赋值后相互影响的问题。
    • 但是浅拷贝只进行一层复制,深层次的引用类型还是共享内存地址。源对象和拷贝对象还是会相互影响
    • 深拷贝就是无限层级拷贝,深拷贝后的原对象不会和和拷贝对象互相影响。

    实现浅拷贝的方式有哪些呢?

    1. Object.assign
    2. 数组的slice和concat方法
    3. 数组静态方法Array.from
    4. 扩展运算符

    实现深拷贝

    要求:

    • 支持对象、数组、日期、正则的拷贝。
    • 处理Map 和 Set
    • 处理原始类型(原始类型直接返回,只有引用类型才有深拷贝这个概念)。
    • 处理 Symbol 作为键名的情况。
    • 处理函数(函数直接返回,拷贝函数没有意义,两个对象使用内存中同一个地址的函数,没有任何问题)。
    • 处理 DOM 元素(DOM 元素直接返回,拷贝 DOM 元素没有意义,都是指向页面中同一个)。
    • 额外开辟一个储存空间 WeakMap,解决循环引用递归爆栈问题(引入 WeakMap 的另一个意义,配合垃圾回收机制,防止内存泄漏)。

    我们要实现的目标:

    const target = {
        a: true,
        b: 100,
        c: 'str',
        d: undefined,
        e: null,
        f:new Date(),
        g: /abc/,
        h:{
            a:'ccc',
            b:12
        },
        i:[1,2,3,4],
        j:Symbol("age"),
        k:new Set([1,2,3,4]),
        l:new Map([[1,2],[3,4]]),
        [nameSymbol]:"job",
    };
    target.target = target;
    
    

    1.最简单的实现,不考虑引用类型

    要拷贝的对象

    const target = {
          a: true,
          b: 100,
          c: 'str',
          d: undefined,
    };
    

    要拷贝这个对象,我们只需要把对象里面的数据按个拷贝出来就可以了。

    function deepClone(target){
        let cloneTarget = {};
        for(const key in target){
            cloneTarget[key] = target[key];
        }
        return cloneTarget;
    }
    

    2.判断处理 Null的情况

    const target = {
          a: true,
          b: 100,
          c: 'str',
          d: undefined,
          e: null,
    };
    

    我们只需要添加一个判断就可以了。

    function deepClone(target){
        //如果是null 就返回
        if(target === null){
            return target;
        }
        let cloneTarget = {};
        for(const key in target){
            cloneTarget[key] = target[key];
        }
        return cloneTarget;
    }
    

    3.判断处理日期的情况

    const target = {
        a: true,
        b: 100,
        c: 'str',
        d: undefined,
        e: null,
        f:new Date(),
    };
    

    日期是一个对象,我们可以通过日期的构造函数重新new一个对象来进行复制

    function deepClone(target){
        //如果是null 就返回
        if(target === null){
            return target;
        }
        if(target instanceof Date){
            return new Date(target);
        }
        let cloneTarget = {};
        for(const key in target){
            cloneTarget[key] = target[key];
        }
        return cloneTarget;
    }
    

    4.判断处理正则的情况

    const target = {
        a: true,
        b: 100,
        c: 'str',
        d: undefined,
        e: null,
        f:new Date(),
        g: /abc/,
    };
    

    正则和日期一样,同样也可以使用构造函数来处理。

    function deepClone(target){
        //如果是null 就返回
        if(target === null){
            return target;
        }
        if(target instanceof Date){
            return new Date(target);
        }
        if(target instanceof RegExp){
            return new RegExp(target);
        }
        let cloneTarget = {};
        for(const key in target){
            cloneTarget[key] = target[key];
        }
        return cloneTarget;
    }
    

    5.深拷贝复制引用类型

    const target = {
        a: true,
        b: 100,
        c: 'str',
        d: undefined,
        e: null,
        f:new Date(),
        g: /abc/,
        h:{
            a:'ccc',
            b:12
        }
    };
    

    里面既然有了引用类型,那么我们只需要递归调用一下,然后返回就可以了。
    arguments.callee 这里指向函数的引用。

    function deepClone(target){
        
        //如果是null 就返回
        if(target === null){
            return target;
        }
        if(target instanceof Date){
            return new Date(target);
        }
        if(target instanceof RegExp){
            return new RegExp(target);
        }
        
        //处理引用类型 以免死循环
        if(typeof target !== 'object'){
            return target;
        }
       
        const cloneTarget = {} 
        for(const key in target){
            cloneTarget[key] = deepClone(target[key]);
        }
        return cloneTarget;
    }
    

    6.处理数组的情况

    const target = {
        a: true,
        b: 100,
        c: 'str',
        d: undefined,
        e: null,
        f:new Date(),
        g: /abc/,
        h:{
            a:'ccc',
            b:12
        },
        i:[1,2,3,4]
    };
    

    数组这了比较简单,它的区别仅仅只是我们拷贝的是对象还是数组。

    function deepClone(target){
        
        //如果是null 就返回
        if(target === null){
            return target;
        }
        if(target instanceof Date){
            return new Date(target);
        }
        if(target instanceof RegExp){
            return new RegExp(target);
        }
        //处理引用类型 以免死循环
        if(typeof target !== 'object'){
            return target;
        }
        // 处理对象和数组 以及原型链
        const cloneTarget = new target.constructor() // 创建一个新的克隆对象或克隆数组
        for(const key in target){
            cloneTarget[key] = deepClone(target[key]);
        }
        return cloneTarget;
    }
    

    可以看到上面有一个骚操作,就是我们通过实例的constructor拿到它的构造函数,然后直接new 就可以了。
    这样就不用在去判断是否是数组还是对象,然后去调用它对应的构造函数,当然这里这样写有一定的风险。如果作为底层库来使用,需要考虑constructor并没有指向它构造函数的情况。

    不过通过它的构造函数我们解决了另外一个问题,就是原型链。通过它原本的构造函数,原型链自然也是完整保存的。意想不到的小细节

    7.处理Symbol的情况

    const target = {
        a: true,
        b: 100,
        c: 'str',
        d: undefined,
        e: null,
        f:new Date(),
        g: /abc/,
        h:{
            a:'ccc',
            b:12
        },
        i:[1,2,3,4],
        j:Symbol("age"),
        [Symbol("name")]:"job",
    };
    

    Symbol的特性就是全局唯一值,里面的参数只是一个描述符。而并不是具体值。这里在处理的时候,我们需要考虑两种情况。

    1. 一种是作为值存在的Symbol
    2. 第二种是作为键存在的Symbol

    作为值存在的话,我们可以通过Symbol.prototype.toString方法拿到它的描述字段。然后重新构造一个Symbol

    //处理值为Symbol的情况
        if(typeof target === 'symbol'){
            return Symbol(target.toString());
        }
    

    作为键存在的话,for in的遍历范围就无法满足我们的要求的,所以需要换成遍历范围更加合适的Reflect.ownKeys()

    下面是MDN的原话

    Reflect.ownKeys 方法返回一个由目标对象自身的属性键组成的数组。
    它的返回值等同于Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))。
    

    同时使用Reflect.ownKeys解决了for in的一个隐藏的问题。那就是会把原型对象的属性也遍历下来,然后存储到拷贝对象里面。

    升级后的效果:

    
    function deepClone(target,map = new WeakMap()){
        //如果是null 就返回
        if(target === null){
            return target;
        }
        //处理日期
        if(target instanceof Date){
            return new Date(target);
        }
        //处理正则表达式
        if(target instanceof RegExp){
            return new RegExp(target);
        }
        //处理值为Symbol的情况
        if(typeof target === 'symbol'){
            return Symbol(target.toString());
        }
        //处理引用类型 以免死循环
        if(typeof target !== 'object'){
            return target;
        }
        // 处理对象和数组
        const cloneTarget = new target.constructor() // 创建一个新的克隆对象或克隆数组
        //通过Reflect来拿到所有可枚举和不可枚举的属性,以及Symbol的属性。
        Reflect.ownKeys(target).forEach(key=>{
            cloneTarget[key] = deepClone(target[key]);
        })
        return cloneTarget;
    }
    

    8.处理循环引用的情况

    const target = {
        a: true,
        b: 100,
        ...
    };
    
    target.target = target;
    

    数据是上面的这样。这种的数据会让递归进入死循环从而造成爆栈的问题。
    这里可以通过WeakMap来建立当前对象和拷贝对象的弱引用关系,判断当前对象是否存在,如果存在就使用它
    保存的对象,如果不存在就添加进去。

    WeakMap的原理是,它的键值只能是引用类型,它的这个引用类型并不会强制标记,当垃圾回收机制需要释放内存的时候,它会被直接释放,而不需要做其他的操作,使用者也不担心内存泄漏的问题。

    我们用WeakMap改造升级一下。

    function deepClone(target,map = new WeakMap()){
        //如果是null 就返回
        if(target === null){
            return target;
        }
        //处理日期
        if(target instanceof Date){
            return new Date(target);
        }
        //处理正则表达式
        if(target instanceof RegExp){
            return new RegExp(target);
        }
        //处理值为Symbol的情况
        if(typeof target === 'symbol'){
            return Symbol(target.toString());
        }
        //处理引用类型 以免死循环
        if(typeof target !== 'object'){
            return target;
        }
        //判断释放存在
        if(map.has(target)){
            return target;
        }
        // 处理对象和数组
        const cloneTarget = new target.constructor() // 创建一个新的克隆对象或克隆数组
        //保存原引用和拷贝引用的关系
        map.set(target,cloneTarget)
        //通过Reflect来拿到所有可枚举和不可枚举的属性,以及Symbol的属性。
        Reflect.ownKeys(target).forEach(key=>{
            cloneTarget[key] = deepClone(target[key],map);
        })
        return cloneTarget;
    }
    

    这样就可以处理循环引用的问题了。

    9.处理 Set和Map的情况 和HTMLElement的情况

    因为这两个都是可迭代的数据结构,同时它们又有自己的添加属性的方法。所以需要按个判断。

    由于之前 const cloneTarget = new target.constructor(),我们并不需要去手动添加处理MapSet

    而HTMLElement的情况并不需要处理,拷贝也没有意义,直接返回就好。

    function deepClone(target,map = new WeakMap()){
        //如果是null 就返回
        if(target === null) return target;
        //处理日期
        if(target instanceof Date)  return new Date(target);
        //处理正则表达式
        if(target instanceof RegExp)  return new RegExp(target);
        //处理值为Symbol的情况
        if(typeof target === 'symbol') return Symbol(target.toString());
        // 处理 DOM元素
        if (target instanceof HTMLElement) return target
        //非引用类型的直接返回 比如函数 就不需要处理,直接返回就好
        if(typeof target !== 'object') return target;
        //从缓冲中读取
        if(map.has(target)) return target;
        // 处理对象和数组
        const cloneTarget = new target.constructor() // 创建一个新的克隆对象或克隆数组
        //保存原引用和拷贝引用的关系
        map.set(target,cloneTarget)
        //处理Map的情况
        if(target instanceof Map){
           for(let [key,value] of target){
               target.set(key,deepClone(value,map));
           }
           return target;
        }
        //处理set的情况
        if(target instanceof Set){
            for(let value of target){
                target.add(deepClone(value,map))
            }
            return target;
        }
        //通过Reflect来拿到所有可枚举和不可枚举的属性,以及Symbol的属性。
        Reflect.ownKeys(target).forEach(key=>{
            cloneTarget[key] = deepClone(target[key],map);
        })
        return cloneTarget;
    }
    

    差不多就是这样了。我们处理了下面的情况:

    const target = {
        a: true,
        b: 100,
        c: 'str',
        d: undefined,
        e: null,
        f:new Date(),
        g: /abc/,
        h:{
            a:'ccc',
            b:12
        },
        i:[1,2,3,4],
        j:Symbol("age"),
        k:new Set([1,2,3,4]),
        l:new Map([[1,2],[3,4]]),
        [nameSymbol]:"job",
    };
    target.target = target;
    

    打印结果:

    {
      a: true,
      b: 100,
      c: 'str',
      d: undefined,
      e: null,
      f: 2022-03-30T09:47:13.762Z,
      g: /abc/,
      h: { a: 'ccc', b: 12 },
      i: [ 1, 2, 3, 4 ],
      j: Symbol(Symbol(age)),
      k: Set(4) { 1, 2, 3, 4 },
      l: Map(2) { 1 => 2, 3 => 4 },
      target: <ref *1> {
        a: true,
        b: 100,
        //这里省略折起...
      },
      [Symbol(name)]: 'job'
    }
    
    

    如果真的要实现一个深拷贝 ,那么情况要复杂的多,可以参考lodash的源码学习。

    聊一下另外一个深拷贝的方式: JSON.parse(JSON.stringify(target))

    我们使用这个来实现深拷贝 ,直接深拷贝上面的测试用例,看看能够有几个?

    const target = {
        0:NaN,
        1:Infinity,
        2:-Infinity,
        a: true,
        b: 100,
        c: 'str',
        d: undefined,
        e: null,
        f:new Date(),
        g: /abc/,
        h:{
            a:'ccc',
            b:12
        },
        i:[1,2,3,4],
        j:Symbol("age"),
        k:new Set([1,2,3,4]),
        l:new Map([[1,2],[3,4]]),
        [nameSymbol]:"job",
    };
    target.target = target;
    JSON.parse(JSON.stringify(target))
    //报错:
    VM8398:1 Uncaught TypeError: Converting circular structure to JSON
        --> starting at object with constructor 'Object'
        --- property 'target' closes the circle
        at JSON.stringify (<anonymous>)
        at <anonymous>:1:17
    

    意思循环引用错误。我们把循环引用的代码去掉:

    target.target = target;
    

    看一下打印出来的结果:

    '0': null,
    '1': null,
    '2': null,
    a: true
    b: 100
    c: "str"
    e: null
    f: "2022-03-30T11:20:08.362Z"
    g: {}
    h: {a: 'ccc', b: 12}
    i: (4) [1, 2, 3, 4]
    k: {}
    l: {}
    [[Prototype]]: Object
    

    从头看到尾:

    1. d: undefined,没有了,无法处理值 为undefined的情况
    2. f:2022-03-30T11:20:08.362Z时间变成了字符串
    3. g: {} 正则表达式 没有了
    4. j:Symbol("age") 所有Symbol的值都没有了,
    5. k:new Set([1,2,3,4]) Set 没有了
    6. l:new Map([[1,2],[3,4]]) Map 没有了

    会忽略的有 : undefined,Symbol,函数 ,直接不存在
    会变成对象:Map,Set,正则表达式
    会被序列化为Null:NaNInfinity-Infinity

    不能循环引用。

    学习参考的文章:感谢这些大佬,我才能站在巨人的肩膀上。
    轻松拿下 JS 浅拷贝、深拷贝
    如何写出一个惊艳面试官的深拷贝?

    相关文章

      网友评论

          本文标题:不积跬步之手写深拷贝

          本文链接:https://www.haomeiwen.com/subject/laywjrtx.html