js 中的拷贝分为三种:
赋值(=)
基本数据类型:赋值,赋值之后两个变量互不干扰;
引用数据类型:赋址,仅改变引用的指针,指向同一个对象,所以相互之间有影响;
浅拷贝
重新在堆中创建内存,拷贝前后对象的基本数据类型互不影响;
只拷贝一层,不能对对象中的子对象进行拷贝;
深拷贝
对对象中的子对象进行递归拷贝;
拷贝前后的两个对象互不影响;
1. 赋值和浅拷贝的区别 直接上代码(copy别人的例子):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 var obj1 = { 'name' : 'zhangsan' , 'age' : '18' , 'language' : [1 ,[2 ,3 ],[4 ,5 ]], }; var obj2 = obj1;var obj3 = shallowCopy (obj1);function shallowCopy (src ) { var dst = {}; for (var prop in src) { if (src.hasOwnProperty (prop)) { dst[prop] = src[prop]; } } return dst; } obj2.name = "lisi" ; obj3.age = "20" ; obj2.language [1 ] = ["二" ,"三" ]; obj3.language [2 ] = ["四" ,"五" ]; console .log (obj1); console .log (obj2);console .log (obj3);
先定义个一个原始的对象 obj1,然后使用赋值得到第二个对象 obj2,然后通过浅拷贝,将 obj1 里面的属性都赋值到 obj3 中。也就是说:
obj1:原始数据
obj2:赋值操作得到
obj3:浅拷贝得到
然后我们改变 obj2 的 name 属性和 obj3 的 name 属性,可以看到,改变赋值得到的对象 obj2 同时也会改变原始值 obj1,而改变浅拷贝得到的的 obj3 则不会改变原始对象 obj1。这就可以说明赋值得到的对象 obj2 只是将指针改变,其引用的仍然是同一个对象,而浅拷贝得到的的 obj3 则是重新创建了新对象。
然而,我们接下来来看一下改变引用类型会是什么情况呢,我又改变了赋值得到的对象 obj2 和浅拷贝得到的 obj3 中的 language 属性的第二个值和第三个值(language 是一个数组,也就是引用类型)。结果见输出,可以看出来,无论是修改赋值得到的对象 obj2 和浅拷贝得到的 obj3 都会改变原始数据。
这是因为浅拷贝只复制一层对象的属性,并不包括对象里面的为引用类型的数据。所以就会出现改变浅拷贝得到的 obj3 中的引用类型时,会使原始数据得到改变。
1.1 浅拷贝:Object.assign() Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。
1 2 3 4 5 6 7 var x = { a: 1, b: { f: { g: 1 } }, c: [ 1, 2, 3 ] }; var y = Object.assign({}, x); console.log(y.b.f === x.b.f); // true
2. 深拷贝 2.1 深拷贝与浅拷贝的区别
浅拷贝:将 B 对象拷贝到 A 对象中,但不包括 B 里面的子对象;
深拷贝:将 B 对象拷贝到 A 对象中,包括 B 里面的子对象;
2.2 Array的 slice 和 concat 方法 Array的slice和concat方法不修改原数组,只会返回一个浅复制了原数组中的元素的一个新数组。之所以把它放在深拷贝里,是因为它看起来像是深拷贝。而实际上它是浅拷贝 。原数组的元素会按照下述规则拷贝:
如果该元素是个对象引用 (不是实际的对象),slice 会拷贝这个对象引用到新的数组里。两个对象引用都引用了同一个对象。如果被引用的对象发生改变,则新的和原来的数组中的这个元素也会发生改变。
对于字符串、数字及布尔值来说(不是 String、Number 或者 Boolean 对象),slice 会拷贝这些值到新的数组里。在别的数组里修改这些字符串或数字或是布尔值,将不会影响另一个数组。
如果向两个数组任一中添加了新元素,则另一个不会受到影响。例子如下:
1 2 3 4 5 6 7 var array = [1 ,2 ,3 ]; var array_shallow = array; var array_concat = array.concat (); var array_slice = array.slice (0 ); console .log (array === array_shallow); console .log (array === array_slice); console .log (array === array_concat);
可以看出,concat和slice返回的不同的数组实例,这与直接的引用复制是不同的。而从另一个例子可以看出Array的concat和slice并不是真正的深复制,数组中的对象元素(Object,Array等)只是复制了引用。如下:
1 2 3 4 5 6 7 8 9 var array = [1 , [1 ,2 ,3 ], {name :"array" }]; var array_concat = array.concat ();var array_slice = array.slice (0 );array_concat[1 ][0 ] = 5 ; console .log (array[1 ]); console .log (array_slice[1 ]); array_slice[2 ].name = "array_slice" ; console .log (array[2 ].name ); console .log (array_concat[2 ].name );
2.3 JSON对象的 parse 和 stringify JSON对象是ES5中引入的新的类型(支持的浏览器为IE8+),JSON对象parse方法可以将JSON字符串反序列化成JS对象,stringify方法可以将JS对象序列化成JSON字符串,借助这两个方法,也可以实现对象的深拷贝。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 var source = { name :"source" , child :{ name :"child" } } var target = JSON .parse (JSON .stringify (source));target.name = "target" ; console .log (source.name ); console .log (target.name ); target.child .name = "target child" ; console .log (source.child .name ); console .log (target.child .name ); var source = { name :function ( ){console .log (1 );}, child :{ name :"child" } } var target = JSON .parse (JSON .stringify (source));console .log (target.name ); var source = { name :function ( ){console .log (1 );}, child :new RegExp ("e" ) }var target = JSON .parse (JSON .stringify (source));console .log (target.name ); console .log (target.child );
这种方法使用较为简单,可以满足基本的深拷贝需求,而且能够处理JSON格式能表示的所有数据类型,但是对于正则表达式类型、函数类型等无法进行深拷贝(而且会直接丢失相应的值)。还有一点不好的地方是它会抛弃对象的constructor。也就是深拷贝之后,不管这个对象原来的构造函数是什么,在深拷贝之后都会变成Object。同时如果对象中存在循环引用的情况也无法正确处理。
2.4 jQuery.extend()方法源码实现深拷贝 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 jQuery.extend = jQuery.fn .extend = function ( ) { var options, name, src, copy, copyIsArray, clone, target = arguments [0 ] || {}, i = 1 , length = arguments .length , deep = false ; if (typeof target === "boolean" ) { deep = target; target = arguments [1 ] || {}; i++; } if (typeof target !== "object" && !jQuery.isFunction (target)) { target = {}; } if (length === i) { target = this ; --i; } for (; i < length; i++) { if ((options = arguments [i]) != null ) { for (name in options) { src = target[name]; copy = options[name]; if (target === copy) { continue ; } if (deep && copy && (jQuery.isPlainObject (copy) || (copyIsArray = jQuery.isArray (copy)))) { if (copyIsArray) { copyIsArray = false ; clone = src && jQuery.isArray (src) ? src : []; } else { clone = src && jQuery.isPlainObject (src) ? src : {}; } target[name] = jQuery.extend (deep, clone, copy); } else if (copy !== undefined ) { target[name] = copy; } } } } return target; };
jQuery的extend方法使用基本的递归思路实现了浅拷贝和深拷贝,但是这个方法也无法处理源对象内部循环引用,例如:
1 2 3 4 5 var a = {"name" :"aaa" };var b = {"name" :"bbb" };a.child = b; b.parent = a; $.extend (true ,{},a);
3. 参考