最近的学习中,仔细研究了下深拷贝和浅拷贝,下面就来简单的总结下。
数据类型
首先我们了解下两种数据类型:
1、基本类型:像 Number、String、Boolean 等这种为基本类型
2、复杂类型:Object 和 Array
浅拷贝与深拷贝的概念
接着我们分别来了解下浅拷贝和深拷贝,深拷贝和浅拷贝是只针对 Object 和 Array 这样的复杂类型的。
浅拷贝:
1 2 3 4 5 6 7 8 |
var a = { myname: 'yana' }; var b = a; b.myname = '小雅'; console.log(b.myname); // 小雅 console.log(a.myname); // 小雅 |
1 2 3 4 5 6 |
var a = ['myname', 'yana']; var b = a; b[1] = '小雅'; console.log(a); // ["myname", "小雅"] console.log(b); // ["myname", "小雅"] |
可以看出,对于对象或数组类型,当我们将 a 赋值给 b,然后更改 b 中的属性,a 也会随着变化。
也就是说 a 和 b 指向了同一块内存,所以修改其中任意的值,另一个值都会随之变化,这就是浅拷贝。
深拷贝:
刚刚我们了解了什么是浅拷贝,那么相应的,如果给 b 放到新的内存中,将 a 的各个属性都复制到新内存里,就是深拷贝。
也就是说,当 b 中的属性有变化的时候,a 内的属性不会发生变化。
浅拷贝
那么除了上面简单的赋值引用,还有哪些方法使用了浅拷贝呢?
Object.assign()
在 MDN 上介绍 Object.assign():"Object.assign() 方法用于将所有可枚举的属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。"
复制一个对象
1 2 3 4 5 6 7 |
var target = {a: 1, b: 1}; var copy1 = {a: 2, b: 2, c: {ca: 21, cb: 22, cc: 23}}; var copy2 = {c: {ca: 31, cb: 32, cd: 34}}; var result = Object.assign(target, copy1, copy2); console.log(target); // {a: 2, b: 2, c: {ca: 31, cb: 32, cc: 33}} console.log(target === result); // true |
可以看到,Object.assign() 拷贝的只是属性值,假如源对象的属性值是一个指向对象的引用,它也只拷贝那个引用值。所以 Object.assign() 只能用于浅拷贝或是合并对象。这是 Object.assign() 值得注意的地方。
深拷贝
那么下面我们就来说说复杂的深拷贝。
jQuery.extend()
说到深拷贝,第一想到的就是 jQuery.extend()方法,下面我们简单看下 jQuery.extend() 的使用。
jQuery.extend( [deep ], target, object1 [, objectN ] ),其中 deep 为 Boolean 类型,如果是 true,则进行深拷贝。
我们还是用上面的数据来看下 extend() 方法。
1 2 3 4 5 6 |
var target = {a: 1, b: 1}; var copy1 = {a: 2, b: 2, c: {ca: 21, cb: 22, cc: 23}}; var copy2 = {c: {ca: 31, cb: 32, cd: 34}}; var result = $.extend(true, target, copy1, copy2); // 进行深拷贝 console.log(target); // {a: 2, b: 2, c: {ca: 31, cb: 32, cc: 23, cd: 34}} |
1 2 3 4 5 6 |
var target = {a: 1, b: 1}; var copy1 = {a: 2, b: 2, c: {ca: 21, cb: 22, cc: 23}}; var copy2 = {c: {ca: 31, cb: 32, cd: 34}}; var result = $.extend(target, copy1, copy2); // 不进行深拷贝 console.log(target); // {a: 1, b: 1, c: {ca: 31, cb: 32, cd:34}} |
通过上面的对比可以看出,当使用 extend() 进行深拷贝的时候,对象的所有属性都添加到 target 中了。
我们知道了 extend() 可以进行深拷贝,那么 extend() 是如何实现深拷贝的呢?
先来看下 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 61 62 63 64 65 66 67 |
jQuery.extend = jQuery.fn.extend = function() { var options, name, src, copy, copyIsArray, clone, target = arguments[ 0 ] || {}, i = 1, length = arguments.length, deep = false; // Handle a deep copy situation if ( typeof target === "boolean" ) { deep = target; // Skip the boolean and the target target = arguments[ i ] || {}; i++; } // Handle case when target is a string or something (possible in deep copy) if ( typeof target !== "object" && !jQuery.isFunction( target ) ) { target = {}; } // Extend jQuery itself if only one argument is passed if ( i === length ) { target = this; i--; } for ( ; i < length; i++ ) { // Only deal with non-null/undefined values if ( ( options = arguments[ i ] ) != null ) { // Extend the base object for ( name in options ) { src = target[ name ]; copy = options[ name ]; // Prevent never-ending loop if ( target === copy ) { continue; } // Recurse if we're merging plain objects or arrays if ( deep && copy && ( jQuery.isPlainObject( copy ) || ( copyIsArray = Array.isArray( copy ) ) ) ) { if ( copyIsArray ) { copyIsArray = false; clone = src && Array.isArray( src ) ? src : []; } else { clone = src && jQuery.isPlainObject( src ) ? src : {}; } // Never move original objects, clone them target[ name ] = jQuery.extend( deep, clone, copy ); // Don't bring in undefined values } else if ( copy !== undefined ) { target[ name ] = copy; } } } } // Return the modified object return target; }; |
主要看下关于深拷贝的部分,取第一个参数,如果是 boolean 类型的,就赋值给 deep,下面如果 deep 为 true(也就是进行深拷贝),就递归调用 extend(),这样就将对象的所有属性都添加到了 target 中实现了深拷贝。
JSON.parse() 和 JSON.stringify()
上面的 jQuery 源码是否让你眼花缭乱?有没有什么办法无脑实现深拷贝呢?JSON.parse() 和 JSON.stringify() 给了我们一个基本的解决办法。
1 2 3 4 5 6 7 8 |
var target = {a: 1, b: 1, c: {ca: 11, cb: 12, cc: 13}}; var targetCopy = JSON.parse(JSON.stringify(target)); targetCopy.a = 2; targetCopy.c.ca = 21; console.log(target); // {a: 1, b: 1, c: {ca: 11, cb: 12, cc: 13}} console.log(targetCopy); // {a: 2, b: 1, c: {ca: 21, cb: 12, cc: 13}} console.log(target === targetCopy); // false |
可以看到改变 targetCopy 并没有改变原始的 target,继承的属性也没有丢失,因此实现了基本的深拷贝。
但是用 JSON.parse() 和 JSON.stringify() 会有一个问题。
JSON.parse() 和 JSON.stringify() 能正确处理的对象只有 Number、String、Array 等能够被 json 表示的数据结构,因此函数这种不能被 json 表示的类型将不能被正确处理。
1 2 3 4 5 6 7 8 9 10 |
var target = { a: 1, b: 2, hello: function() { console.log("Hello, world!"); } }; var copy = JSON.parse(JSON.stringify(target)); console.log(copy); // {a: 1, b: 2} |
上面的例子可以看出,hello 这个属性由于是函数类型,使用 JSON.parse() 和 JSON.stringify() 后丢失了。
因此 JSON.parse() 和 JSON.stringify() 还是需要谨慎使用。
下篇文章我会继续为大家说明深拷贝的各种实现。
未完待续......
kuro 2018 年 3 月 18 日
请问关于《深拷贝与浅拷贝的实现》下一篇博文在哪呀..:(,,•́ . •̀,,)..
烽火 2017 年 11 月 2 日
你这个深拷贝太复杂了 ,你可以考虑下迭代
aman 2017 年 9 月 1 日
不错,有时候浅拷贝带来的问题不是那么容易发现。
幾米 2017 年 8 月 28 日
var target = {a: 1, b: 1};
var copy1 = {a: 2, b: 2, c: {ca: 21, cb: 22, cc: 23}};
var copy2 = {c: {ca: 31, cb: 32, cd: 34}};
var result = $.extend(target, copy1, copy2); // 不进行深拷贝
console.log(target); // {a: 1, b: 1, c: {ca: 31, cb: 32, cd:34}}
这个结果错了吧
应该是:{a: 2, b: 2, c: {ca: 31, cb: 32, cd:34}}
target 作为合并容器,属性相同的值会被最新的覆盖吧。
孟陬 2017 年 9 月 5 日
是的,错了
Jalever 2017 年 8 月 21 日
浅拷贝中的代码:
a:2,b:2,c:{ca:31,cb:32,cc:34}
Mutoo 2017 年 8 月 17 日
深拷贝还必须保证原有对象的内部联系,以下测试用例:
var foo = {};
var origin = [foo, foo];
assert(origin[0] === origin[1]);
var copy = deepClone(origin);
assert(copy[0] === copy[1]);
JSON.stringify()/JSON.parse() 会创建两个不同的 foo 对象,所以不能通过该测试用例。
Confidence 2017 年 8 月 17 日
第一个代码,哪儿来的 33
Andy 2017 年 8 月 16 日
简单的深拷贝 Object.assign({},obj)
deepred 2017 年 9 月 8 日
Object.assign 是浅拷贝啊
leslie 2017 年 9 月 20 日
Object.assign 为什么是浅拷贝?
leslie 2017 年 9 月 20 日
对象中属性包含对象引用的时候,Object.assign 还是用的相同引用
tomastong 2019 年 6 月 25 日
里面有对象还是浅拷贝
var a = { myname: ‘yana’,b: {name: ‘bb’} }
var c = Object.assign({}, a )
a.b.name = ‘andy’
console.log(c) // {myname: ‘yaya’, b: {name: ‘andy’}}
sharkrice 2017 年 8 月 11 日
JSON.stringify 的加上个回调函数应该可以把函数 tostring 输出出来吧