zoukankan      html  css  js  c++  java
  • 实现深拷贝的几种方式

    目前使用过四种

    1、递归递归去复制所有层级属性

    function deepClone(obj){
        let objClone = Array.isArray(obj)?[]:{};
        if(obj && typeof obj==="object"){
            for(key in obj){
                if(obj.hasOwnProperty(key)){
                    //判断ojb子元素是否为对象,如果是,递归复制
                    if(obj[key]&&typeof obj[key] ==="object"){
                        objClone[key] = deepClone(obj[key]);
                    }else{
                        //如果不是,简单复制
                        objClone[key] = obj[key];
                    }
                }
            }
        }
        return objClone;
    }    
    let a=[1,2,3,4],
        b=deepClone(a);
    a[0]=2;
    console.log(a,b);

    跟之前想象的一样,现在b脱离了a的控制,不再受a影响了。

    这里再次强调,深拷贝,是拷贝对象各个层级的属性,可以看个例子。JQ里有一个extend方法也可以拷贝对象,我们来看看

    let a=[1,2,3,4],
        b=a.slice();
    a[0]=2;
    console.log(a,b);

    那是不是说slice方法也是深拷贝了,毕竟b也没受a的影响,上面说了,深拷贝是会拷贝所有层级的属性,还是这个例子,我们把a改改

    let a=[0,1,[2,3],4],
            b=a.slice();
    a[0]=1; a[2][0]=1; console.log(a,b);

    拷贝的不彻底啊,b对象的一级属性确实不受影响了,但是二级属性还是没能拷贝成功,仍然脱离不了a的控制,说明slice根本不是真正的深拷贝。

    这里引用知乎问答里面的一张图

     

    第一层的属性确实深拷贝,拥有了独立的内存,但更深的属性却仍然公用了地址,所以才会造成上面的问题。

    同理,concat方法与slice也存在这样的情况,他们都不是真正的深拷贝,这里需要注意。

    2.除了递归,我们还可以借用JSON对象的parse和stringify

    复制代码
    function deepClone(obj){
        let _obj = JSON.stringify(obj),
            objClone = JSON.parse(_obj);
        return objClone
    }    
    let a=[0,1,[2,3],4],
        b=deepClone(a);
    a[0]=1;
    a[2][0]=1;
    console.log(a,b);
    复制代码

    可以看到,这下b是完全不受a的影响了。

    附带说下,JSON.stringify与JSON.parse除了实现深拷贝,还能结合localStorage实现对象数组存储。有兴趣可以阅读博主这篇文章。

    localStorage存储数组,对象,localStorage,sessionStorage存储数组对象

    3.除了上面两种方法之外,我们还可以借用JQ的extend方法。

    $.extend( [deep ], target, object1 [, objectN ] )

    deep表示是否深拷贝,为true为深拷贝,为false,则为浅拷贝

    target Object类型 目标对象,其他对象的成员属性将被附加到该对象上。

    object1  objectN可选。 Object类型 第一个以及第N个被合并的对象。 

    let a=[0,1,[2,3],4],
        b=$.extend(true,[],a);
    a[0]=1;
    a[2][0]=1;
    console.log(a,b);

    可以看到,效果与上面方法一样,只是需要依赖JQ库。

    说了这么多,了解深拷贝也不仅仅是为了应付面试题,在实际开发中也是非常有用的。例如后台返回了一堆数据,你需要对这堆数据做操作,但多人开发情况下,你是没办法明确这堆数据是否有其它功能也需要使用,直接修改可能会造成隐性问题,深拷贝能帮你更安全安心的去操作数据,根据实际情况来使用深拷贝,大概就是这个意思。

    4.lodash的_.cloneDeep()

     参考博客:https://www.cnblogs.com/echolun/p/7889848.html

    以下是我参看的一位关于深拷贝的问题解决。

    JSON.parse

    先将一个对象转为json对象。然后再解析这个json对象。

    let obj = {a:{b:22}};
    let copy = JSON.parse(JSON.stringify(obj));
    

    这种方法的优点就是代码写起来比较简单。但是缺点也是显而易见的。你先是创建一个临时的,可能很大的字符串,只是为了把它重新放回解析器。另一个缺点是这种方法不能处理循环对象。

    如下面的循环对象用这种方法的时候会抛出异常

    let a = {};
    let b = {a};
    a.b = b;
    let copy = JSON.parse(JSON.stringify(a));
    

     

    诸如 Map, Set, RegExp, Date, ArrayBuffer 和其他内置类型在进行序列化时会丢失。

    let a = {};
    let b = new Set();
    b.add(11);
    a.test = b;
    let copy = JSON.parse(JSON.stringify(a));
    

    a 的值打印如下

    copy的值打印如下

    对比发现,Set已丢失。

    Structured Clone 结构化克隆算法

    MessageChannel

    建立两个端,一个端发送消息,另一个端接收消息。

    function structuralClone(obj) {
        return new Promise(resolve =>{
            const {port1, port2} = new MessageChannel();
            port2.onmessage = ev => resolve(ev.data);
            port1.postMessage(obj);
        })
    }
    const obj = /* ... */;
    structuralClone(obj).then(res=>{
         console.log(res);
    })

    这种方法的优点就是能解决循环引用的问题,还支持大量的内置数据类型。缺点就是这个方法是异步的。

    History API

    利用history.replaceState。这个api在做单页面应用的路由时可以做无刷新的改变url。这个对象使用结构化克隆,而且是同步的。但是我们需要注意,在单页面中不要把原有的路由逻辑搞乱了。所以我们在克隆完一个对象的时候,要恢复路由的原状。

    function structuralClone(obj) {
      const oldState = history.state;
      history.replaceState(obj, document.title);
      const copy = history.state;
      history.replaceState(oldState, document.title);
      return copy;
    }
    
    var obj = {};
    var b = {obj};
    obj.b = b
    var copy = structuralClone(obj); 
    console.log(copy);

    这个方法的优点是。能解决循环对象的问题,也支持许多内置类型的克隆。并且是同步的。但是缺点就是有的浏览器对调用频率有限制。比如Safari 30 秒内只允许调用 100 次

     

    Notification API

    这个api主要是用于桌面通知的。如果你使用Facebook的时候,你肯定会发现时常在浏览器的右下角有一个弹窗,对就是这家伙。我们也可以利用这个api实现js对象的深拷贝。

    function structuralClone(obj) {
      return new Notification('', {data: obj, silent: true}).data;
    }
    
    var obj = {};
    var b = {obj};
    obj.b = b
    var copy = structuralClone(obj);
    console.log(copy)

    同样是优点和缺点并存,优点就是可以解决循环对象问题,也支持许多内置类型的克隆,并且是同步的。缺点就是这个需要api的使用需要向用户请求权限,但是用在这里克隆数据的时候,不经用户授权也可以使用。在http协议的情况下会提示你再https的场景下使用。

    lodash的_.cloneDeep()

    支持循环对象,和大量的内置类型,对很多细节都处理的比较不错。推荐使用。

    支持的类型有很多

     

    我们这里再次关注一下lodash是如何解决循环应用这个问题的?

     

    从相关的代码中。我们可以发现。lodash是用一个栈记录了。所有被拷贝的引用值。如果再次碰到同样的引用值的时候,不会再去拷贝一遍。而是利用之前已经拷贝好的值。

    lodash深拷贝的详细的源码可以在这里查看。

    实现一个简易点的深拷贝,以解决循环引用的问题为目标

    我们仅仅实现一个简易点的深拷贝。能优雅的处理循环引用的即可。在实现深拷贝之前,我们首先温习回顾一下js中的遍历对象的属性的方法和各种方法的优缺点。

     

    js中遍历一个对象的属性的方法

    • Object.keys() 仅仅返回自身的可枚举属性,不包括继承来的,更不包括Symbol属性
    • Object.getOwnPropertyNames() 返回自身的可枚举和不可枚举属性。但是不包括Symbol属性
    • Object.getOwnPropertySymbols() 返回自身的Symol属性
    • for...in 可以遍历对象的自身的和继承的可枚举属性,不包含Symbol属性
    • Reflect.ownkeys() 返回对象自身的所有属性,不管是否可枚举,也不管是否是Symbol。注意不包括继承的属性

     

    实现深拷贝,解决循环引用问题

    /**
     * 判断是否是基本数据类型
     * @param value 
     */
    function isPrimitive(value){
      return (typeof value === 'string' || 
      typeof value === 'number' || 
      typeof value === 'symbol' ||
      typeof value === 'boolean')
    }
    
    /**
     * 判断是否是一个js对象
     * @param value 
     */
    function isObject(value){
      return Object.prototype.toString.call(value) === "[object Object]"
    }
    
    /**
     * 深拷贝一个值
     * @param value 
     */
    function cloneDeep(value){
    
      // 记录被拷贝的值,避免循环引用的出现
      let memo = {};
    
      function baseClone(value){
        let res;
        // 如果是基本数据类型,则直接返回
        if(isPrimitive(value)){
          return value;
        // 如果是引用数据类型,我们浅拷贝一个新值来代替原来的值
        }else if(Array.isArray(value)){
          res = [...value];
        }else if(isObject(value)){
          res = {...value};
        }
    
        // 检测我们浅拷贝的这个对象的属性值有没有是引用数据类型。如果是,则递归拷贝
        Reflect.ownKeys(res).forEach(key=>{
          if(typeof res[key] === "object" && res[key]!== null){
            //此处我们用memo来记录已经被拷贝过的引用地址。以此来解决循环引用的问题
            if(memo[res[key]]){
              res[key] = memo[res[key]];
            }else{
              memo[res[key]] = res[key];
              res[key] = baseClone(res[key])
            }
          }
        })
        return res;  
      }
    
      return baseClone(value)
    }

    验证我们写的cloneDeep是否能解决循环应用的问题

    var obj = {};
    var b = {obj};
    obj.b = b
    var copy = cloneDeep(obj); 
    console.log(copy);

    完美。大功告成

    我们虽然的确解决了深拷贝的大部分问题。不过很多细节还没有去处理。在生产环境,我们还是要使用lodash的cloneDeep。cloneDeep对每个数据类型都单独处理的非常好。比如ArrayBuffer什么的。我们都没有处理。

    作者:Jesse
    链接:https://www.zhihu.com/question/23031215/answer/460652947
    来源:知乎
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    参考博客:https://www.zhihu.com/question/23031215(讲的比较好,推荐)

  • 相关阅读:
    django-ForeignKey,OneToOneField,ManyToManyField
    django-HttpResponse,render,redirect
    django-常见问题勘误
    django-个人网站之环境配置(一)
    django-表单之数据保存(七)
    django-表单之模型表单渲染(六)
    django-表单之手动渲染(五)
    Linux内核中_IO,_IOR,_IOW,_IOWR宏的用法与解析
    流媒体传输协议详解之---RTSP认证
    在YUV图像上根据背景色实现OSD反色
  • 原文地址:https://www.cnblogs.com/shj-com/p/13645276.html
Copyright © 2011-2022 走看看