当前位置 博文首页 > jcLee95的博客:Javascript中的对象拷贝(对象复制/克隆)

    jcLee95的博客:Javascript中的对象拷贝(对象复制/克隆)

    作者:[db:作者] 时间:2021-09-18 15:47

    Javascript中的对象拷贝(对象复制/克隆)

    李俊才
    CSDN:jcLee95
    邮箱:291148484@163.com


    1. 对象的引用

    要说“拷贝”还要先说“引用”的概念。
    在JavaScript中没有“指针”的概念,但保留了对象的“引用”。首先必须明确,与“Java”、“Python”等经典面向对象编程语言中“一切皆可对象”不同,在JavaScript中绝非一切皆是对象,并且“引用”是“对象”的引用。这里就需要区分在赋值操作“=”的右侧,到底是一个“字面量”还是一个对象。举例而言:

    var a = 1;
    var b = "Hello World!"
    
    var c = a
    

    这里的数字1和"Hello World!"都并非对象,而是不可变的字面量值。他们分别源于简单基本类型numberstring而不是内置对象NumberString。(不过,对于左侧的变量,如果你要使用一些方法或属性,如.leng,并不需要显式将string转为String对象)

    由于右侧不是对象,这在赋值操作var c = a传递的是值,即将number 1传给变量c,换成var c = b也类似。

    而下例中:

    var d = {
      Hello : 'world'
    };
    
    var e = d;
    

    由于变量d的右侧是一个变量,这时var d = {Hello : 'world'};使得变量d创建了一个引用,即使得d中保存了右侧那个对象的地址,我们可以对变量d间接对右侧那个变量进行操作,如d.Hello。而后一个赋值操作var e = d;传递的是引用的内容,由于第一个赋值使得d引用到了其右侧那个对象的内存地址,后一个赋值只不过是将变量d中所存储的内容地址赋值了一份给变量e而已,因此这时变量ed引用到了同一个对象。

    2. 浅拷贝

    浅拷贝,又称“浅层拷贝”、“浅复制”、“浅层复制”等等。
    一个很常见的通俗说法,“浅层拷贝就是只拷贝一层”,这样的说法其实不太准确。
    浅拷贝的本质特点是:

    • 拷贝原对象后将得到一个新对象;
    • 新对象的将中的所有属性都是原对象中对应属性的一个引用

    因此从表象上看,浅拷贝拷贝出来的新对象中所有属性的值会复制原对象中对应属性的值。例如:

    var obj_a = {
      is_right : true
    }
    
    var obj_b == {
     a : 1,
     b : obj_a
    }
    

    如果获取对象obj_b的浅拷贝得到一个新对象,可以使用ES6提供的Object.assign()方法:

    var new_obj = Object.assign({}, obj_b);
    

    其中Object.assign()方法,第一个参数是目标对象,后面为一个或多个源对象。
    该方法对obj_b实现浅拷贝的过程为,遍历一个或多个源对象(从第二个参数开始)所有可枚举的自有键,并逐个进行“=”完成复制,最后返回目标对象。
    上例中,对于键值对“a:1”,由于数字“1”是一个不可变的值(字面量)而非数字,new_obj的第一个键值对完全相同,故有:

    new_obj.a == 1   // true
    

    然而第二个键值对的值obj_a引用了一个对象,进行“=”复制到new_obj的键值时,传递的时引用的地址,这样使得new_obj对应与键b的值引用到了对象obj_a。即必有:

    new_obj.b === obj_a
    

    那么,为什么称浅层拷贝呢?
    以上我们已经了解了这种拷贝方法的本质,但是如果想要了解这个名字的由来,我们不妨假设上例中,将obj_a修改为一个键值中含有对象的对象。如:

    var obj_c = {
    are_you_ok : true
    }
    
    var obj_a = {
      is_right : true,
      is_ok : obj_c
    }
    
    var obj_b == {
     a : 1,
     b : obj_a
    }
    

    这时对 obj_b 进行浅拷贝得到new_obj后,new_obj.is_ok对应的值为由obj_a.is_ok对应的值obj_c。由于变量obj_c中存储的是对一个对象的引用,这里传递的同样是被引用对象的地址,但地址中所存储的呢欧容不会进一步拷贝,这样在new_obj中,并不会存储一个

    {are_you_ok : true}
    

    这样的实际对象。因此看起来我们“只拷贝了表层”。这就是所谓“浅”拷贝称为的由来。

    这里最后还有一个需要注意的问题,使用Object.assign()方法浅拷贝时由于相当于只是对源对象的所有可枚举的自有键一一进行“=”赋值操作,对于由属性描述符所描述的一些属性的特性是不会被拷贝到目标对象的。如某个属性是否可修改(Writeable)、可配置(Configurable)、可枚举(Enumerable)

    3. 深拷贝

    在JavaScript中,深拷贝说起来有点麻烦,因为里面情况会很比较复杂。
    相比于浅拷贝深拷贝要求要完整地拷贝下底层被引用地对象而不是仅粗略地要求拷贝下引用中对象的地址。正如之前所说通俗一点理解看:

    • 仅用"="将对象引用的赋值给变量时,仅传递了引用的对象地址;
    • 通过“浅拷贝”返回对象时,相当于对源对象最外层的所有可枚举属性进行了“=”操作后获得的新对象;
    • 通过“深拷贝”返回对象时,相当于任意一层不再简单传递引用的对象的内存地址,而是真正意义上拷贝下来每一层对象。

    但是很快你就会发现,如果在一个对象中引用的对象到某层由存在循环性的引用,往往会导致一个死循环。
    另外,在JavaScript中的函数也是对象,我们在JS中不能确定对一个函数进行“深拷贝”是什么,尽管由很多框架给出了自己的定义。

    一种比较好方法是通过JSON序列化来实现深拷贝:

    var obj_b = JSON.parse(JSON.stringify(obj_a))
    

    这种方法也不是万能的,它要求对象必须是JSON安全的,即:
    不仅obj_a可以被序列化为一个JSON格式的字符串,同时还可以由该字符串解析得到一个结构完全相同的对象。

    cs