zoukankan      html  css  js  c++  java
  • JS中如何进行对象的深拷贝

    在JS中,一般的=号传递的都是对象/数组的引用,并没有真正地拷贝一个对象,那如何进行对象的深度拷贝呢?

    一、对象引用、浅层拷贝与深层拷贝的区别

    js的对象引用传递理解起来很简单,参考如下代码:

    var a = {name:'wanger'}
    var b = a ;
    a===b // true
    b.name = 'zhangsan'
    a.name //'zhangan'

    上述代码中,使用了=进行赋值,于是b指向了a所指向的栈的对象,也就是a与b指向了同一个栈对象,所以在对b.name赋值时,a.name也发生了变化。为了避免上面的情况,可以对对象进行拷贝,代码如下:

    var a = {name:'wanger'}
    var b = Object.assign({}, a)
    a===b // false
    b.name = 'zhangsan'
    a.name //'wanger'

    上面代码将原始对象拷贝到一个空对象,就得到了原始对象的克隆,这时候a与b指向的是不同的栈对象,所以对b.name重新复制也不会影响到a.name。但是如果a.name是一个对象的引用,而不是一个字符串,那么上面的代码也会遇到一些问题,参考如下代码:

    var a = {name:{firstName:'wang',lastName:'er'}}
    var b = Object.assign({}, a)
    a===b // false
    b.name.firstName = 'zhang'
    a.name.firstName //'zhang'

    b.name.firstName又影响到了a.name.firstName,这是因为Object.assign()方法只是浅层拷贝,a.name是一个栈对象的引用,赋值给b时,b.name也同样是这个栈对象的引用,很多时候,我们不想让这种事情发生,所以我们就需要用到对象的深拷贝。

    二、使用JSON.parse()与JSON.stringify()对对象进行拷贝

    通常情况下,我们可以使用JSON.parse()与 JSON.stringify()实现对象的深克隆,如下:

    var clone = function (obj) {
        return JSON.parse(JSON.stringify(obj));
    }

    这种方法只适用于纯数据json对象的深度克隆,因为有些时候,这种方法也有缺陷,参考如下代码:

    var clone = function (obj) {
        return JSON.parse(JSON.stringify(obj));
    }
    var a = {a:function(){console.log('hello world')},b:{c:1},c:[1,2,3],d:"wanger",e:new Date(),f:null,g:undefined}
    var b = clone(a)

    打印如下:

    我们发现,上述的方法会忽略值为function以及undefied的字段,而且对date类型的支持也不太友好。

    更要紧的是,上述方法只能克隆原始对象自身的值,不能克隆它继承的值,参考如下代码:

    function Person (name) {
        this.name = name
    }
    var wanger = new Person('王二')
    var newwanger = clone(wanger)
    wanger.constructor === Person // true
    newwanger.constructor === Object // true

    打印如下:

     我们发现,克隆的对象的构造函数已经变成了Object,而原来的对象的构造是Person。

    三、目前没有发现bug的对象深拷贝方法

    var clone = function (obj) { 
        if(obj === null) return null 
        if(typeof obj !== 'object') return obj;
        if(obj.constructor===Date) return new Date(obj); 
        if(obj.constructor === RegExp) return new RegExp(obj);
        var newObj = new obj.constructor ();  //保持继承链
        for (var key in obj) {
            if (obj.hasOwnProperty(key)) {   //不遍历其原型链上的属性
                var val = obj[key];
                newObj[key] = typeof val === 'object' ? arguments.callee(val) : val; // 使用arguments.callee解除与函数名的耦合
            }
        }  
        return newObj;  
    }; 

    这里有三点需要注意:

    1、用new obj.constructor ()构造函数新建一个空的对象,而不是使用{}或者[],这样可以保持原形链的继承;
    2、用obj.hasOwnProperty(key)来判断属性是否来自原型链上,因为for..in..也会遍历其原型链上的可枚举属性。
    3、上面的函数用到递归算法,在函数有名字,而且名字以后也不会变的情况下,这样定义没有问题。但问题是这个函数的执行与函数名 factorial 紧紧耦合在了一起。为了消除这种紧密耦合的现象,需要使用 arguments.callee

    四、面试恶心人的问题:

      写一个能拷贝自身可枚举、自身不可枚举、自身 Symbol 类型键、原型上可枚举、原型上不可枚举、原型上的 Symol 类型键,循环引用也可以拷的深拷贝函数:

    function cloneDeep(obj) {
        if(obj === null) return null 
        if(typeof obj !== 'object') return obj;
        if(obj.constructor === Date) return new Date(obj); 
        if(obj.constructor === RegExp) return new RegExp(obj);
    
        let family = {}
        let parent = Object.getPrototypeOf(obj)
    
        while (parent != null) {
            family = completeAssign(deepClone(family), parent)
            parent = Object.getPrototypeOf(parent)
        }
    
        // 下面这个函数会拷贝所有自有属性的属性描述符
        function completeAssign(target, ...sources) {
            sources.forEach(source => {
                let descriptors = Object.keys(source).reduce((descriptors, key) => {
                    descriptors[key] = Object.getOwnPropertyDescriptor(source, key)
                    return descriptors
                }, {})
    
                // Object.assign 默认也会拷贝可枚举的Symbols
                Object.getOwnPropertySymbols(source).forEach(sym => {
                    let descriptor = Object.getOwnPropertyDescriptor(source, sym)
                    if (descriptor.enumerable) {
                        descriptors[sym] = descriptor
                    }
                })
                Object.defineProperties(target, descriptors)
            })
            return target
        }
    
        return completeAssign(deepClone(obj), family)
    }

    提示的Object.getOwnPropertyDescriptors中的value是浅拷贝,那么一旦对象中出现了日期、正则这一类特殊对象,就会出现拷贝丢失;所以开头针对特殊对象,需要特殊对待。

    参考:
    1、https://blog.csdn.net/qq_34629352/article/details/105375985

    2、https://blog.csdn.net/ih1107/article/details/79208860

  • 相关阅读:
    el自定义函数库
    DOM4J
    【转载】SqlServer日期时间函数
    【原创】C#认识/理解/运用 StreamReader,StreamWriter,StringReader,StreamWriter
    【原创】C#认识/理解/运用 FileStream
    【原创】C#操作XML(带命名空间)
    【原创】ASP.NET MVC3使用html编辑器(kindeditor)
    【原创】ASP.NET MVC3 从零开始一步步构建Web
    【转载】MVC使用jqGrid
    【原创】C#使用HttpWebRequest,HttpWebResponse
  • 原文地址:https://www.cnblogs.com/art-poet/p/13651833.html
Copyright © 2011-2022 走看看