JavaScript基础心法 深浅拷贝(浅拷贝和深拷贝)
前言
说到深浅拷贝,必须先提到的是JavaScript的数据类型,之前的一篇文章JavaScript基础心法——数据类型说的很清楚了,这里就不多说了。
需要知道的就是一点:JavaScript的数据类型分为基本数据类型和引用数据类型。
对于基本数据类型的拷贝,并没有深浅拷贝的区别,我们所说的深浅拷贝都是对于引用数据类型而言的。
浅拷贝
浅拷贝的意思就是只复制引用,而未复制真正的值。
constoriginArray=[1,2,3,4,5]; constoriginObj={a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}}; constcloneArray=originArray; constcloneObj=originObj; console.log(cloneArray);//[1,2,3,4,5] console.log(originObj);//{a:'a',b:'b',c:Array[3],d:{dd:'dd'}} cloneArray.push(6); cloneObj.a={aa:'aa'}; console.log(cloneArray);//[1,2,3,4,5,6] console.log(originArray);//[1,2,3,4,5,6] console.log(cloneObj);//{a:{aa:'aa'},b:'b',c:Array[3],d:{dd:'dd'}} console.log(originArray);//{a:{aa:'aa'},b:'b',c:Array[3],d:{dd:'dd'}}
上面的代码是最简单的利用=赋值操作符实现了一个浅拷贝,可以很清楚的看到,随着cloneArray和cloneObj改变,originArray和originObj也随着发生了变化。
深拷贝
深拷贝就是对目标的完全拷贝,不像浅拷贝那样只是复制了一层引用,就连值也都复制了。
只要进行了深拷贝,它们老死不相往来,谁也不会影响谁。
目前实现深拷贝的方法不多,主要是两种:
- 利用 JSON 对象中的 parse 和 stringify
- 利用递归来实现每一层都重新创建对象并赋值
JSON.stringify/parse的方法
先看看这两个方法吧:
TheJSON.stringify()methodconvertsaJavaScriptvaluetoaJSONstring.
JSON.stringify是将一个JavaScript值转成一个JSON字符串。
TheJSON.parse()methodparsesaJSONstring,constructingtheJavaScriptvalueorobjectdescribedbythestring.
JSON.parse是将一个JSON字符串转成一个JavaScript值或对象。
很好理解吧,就是JavaScript值和JSON字符串的相互转换。
它能实现深拷贝呢?我们来试试。
constoriginArray=[1,2,3,4,5]; constcloneArray=JSON.parse(JSON.stringify(originArray)); console.log(cloneArray===originArray);//false constoriginObj={a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}}; constcloneObj=JSON.parse(JSON.stringify(originObj)); console.log(cloneObj===originObj);//false cloneObj.a='aa'; cloneObj.c=[1,1,1]; cloneObj.d.dd='doubled'; console.log(cloneObj);//{a:'aa',b:'b',c:[1,1,1],d:{dd:'doubled'}}; console.log(originObj);//{a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};
确实是深拷贝,也很方便。但是,这个方法只能适用于一些简单的情况。比如下面这样的一个对象就不适用:
constoriginObj={ name:'axuebin', sayHello:function(){ console.log('HelloWorld'); } } console.log(originObj);//{name:"axuebin",sayHello:ƒ} constcloneObj=JSON.parse(JSON.stringify(originObj)); console.log(cloneObj);//{name:"axuebin"}
发现在cloneObj中,有属性丢失了。。。那是为什么呢?
在MDN上找到了原因:
Ifundefined,afunction,orasymbolisencounteredduringconversionitiseitheromitted(whenitisfoundinanobject)orcensoredtonull(whenitisfoundinanarray).JSON.stringifycanalsojustreturnundefinedwhenpassingin"pure"valueslikeJSON.stringify(function(){})orJSON.stringify(undefined).
undefined、function、symbol会在转换过程中被忽略。。。
明白了吧,就是说如果对象中含有一个函数时(很常见),就不能用这个方法进行深拷贝。
递归的方法
递归的思想就很简单了,就是对每一层的数据都实现一次创建对象->对象赋值的操作,简单粗暴上代码:
functiondeepClone(source){ consttargetObj=source.constructor===Array?[]:{};//判断复制的目标是数组还是对象 for(letkeysinsource){//遍历目标 if(source.hasOwnProperty(keys)){ if(source[keys]&&typeofsource[keys]==='object'){//如果值是对象,就递归一下 targetObj[keys]=source[keys].constructor===Array?[]:{}; targetObj[keys]=deepClone(source[keys]); }else{//如果不是,就直接赋值 targetObj[keys]=source[keys]; } } } returntargetObj; }
我们来试试:
constoriginObj={a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}}; constcloneObj=deepClone(originObj); console.log(cloneObj===originObj);//false cloneObj.a='aa'; cloneObj.c=[1,1,1]; cloneObj.d.dd='doubled'; console.log(cloneObj);//{a:'aa',b:'b',c:[1,1,1],d:{dd:'doubled'}}; console.log(originObj);//{a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};
可以。那再试试带有函数的:
constoriginObj={ name:'axuebin', sayHello:function(){ console.log('HelloWorld'); } } console.log(originObj);//{name:"axuebin",sayHello:ƒ} constcloneObj=deepClone(originObj); console.log(cloneObj);//{name:"axuebin",sayHello:ƒ}
也可以。搞定。
是不是以为这样就完了??当然不是。
JavaScript中的拷贝方法
我们知道在JavaScript中,数组有两个方法concat和slice是可以实现对原数组的拷贝的,这两个方法都不会修改原数组,而是返回一个修改后的新数组。
同时,ES6中引入了Object.assgn方法和...展开运算符也能实现对对象的拷贝。
那它们是浅拷贝还是深拷贝呢?
concat
Theconcat()methodisusedtomergetwoormorearrays.Thismethoddoesnotchangetheexistingarrays,butinsteadreturnsanewarray.
该方法可以连接两个或者更多的数组,但是它不会修改已存在的数组,而是返回一个新数组。
看着这意思,很像是深拷贝啊,我们来试试:
constoriginArray=[1,2,3,4,5]; constcloneArray=originArray.concat(); console.log(cloneArray===originArray);//false cloneArray.push(6);//[1,2,3,4,5,6] console.log(originArray);[1,2,3,4,5];
看上去是深拷贝的。
我们来考虑一个问题,如果这个对象是多层的,会怎样。
constoriginArray=[1,[1,2,3],{a:1}]; constcloneArray=originArray.concat(); console.log(cloneArray===originArray);//false cloneArray[1].push(4); cloneArray[2].a=2; console.log(originArray);//[1,[1,2,3,4],{a:2}]
originArray中含有数组[1,2,3]和对象{a:1},如果我们直接修改数组和对象,不会影响originArray,但是我们修改数组[1,2,3]或对象{a:1}时,发现originArray也发生了变化。
结论:concat只是对数组的第一层进行深拷贝。
slice
Theslice()methodreturnsashallowcopyofaportionofanarrayintoanewarrayobjectselectedfrombegintoend(endnotincluded).Theoriginalarraywillnotbemodified.
解释中都直接写道是ashallowcopy了~
但是,并不是!
constoriginArray=[1,2,3,4,5]; constcloneArray=originArray.slice(); console.log(cloneArray===originArray);//false cloneArray.push(6);//[1,2,3,4,5,6] console.log(originArray);[1,2,3,4,5];
同样地,我们试试多层的数组。
constoriginArray=[1,[1,2,3],{a:1}]; constcloneArray=originArray.slice(); console.log(cloneArray===originArray);//false cloneArray[1].push(4); cloneArray[2].a=2; console.log(originArray);//[1,[1,2,3,4],{a:2}]
果然,结果和concat是一样的。
结论:slice只是对数组的第一层进行深拷贝。
Object.assign()
TheObject.assign()methodisusedtocopythevaluesofallenumerableownpropertiesfromoneormoresourceobjectstoatargetobject.Itwillreturnthetargetobject.
复制复制复制。
那到底是浅拷贝还是深拷贝呢?
自己试试吧。。
结论:Object.assign()拷贝的是属性值。假如源对象的属性值是一个指向对象的引用,它也只拷贝那个引用值。
...展开运算符
constoriginArray=[1,2,3,4,5,[6,7,8]]; constoriginObj={a:1,b:{bb:1}}; constcloneArray=[...originArray]; cloneArray[0]=0; cloneArray[5].push(9); console.log(originArray);//[1,2,3,4,5,[6,7,8,9]] constcloneObj={...originObj}; cloneObj.a=2; cloneObj.b.bb=2; console.log(originObj);//{a:1,b:{bb:2}}
结论:...实现的是对象第一层的深拷贝。后面的只是拷贝的引用值。
首层浅拷贝
我们知道了,会有一种情况,就是对目标对象的第一层进行深拷贝,然后后面的是浅拷贝,可以称作“首层浅拷贝”。
我们可以自己实现一个这样的函数:
functionshallowClone(source){ consttargetObj=source.constructor===Array?[]:{};//判断复制的目标是数组还是对象 for(letkeysinsource){//遍历目标 if(source.hasOwnProperty(keys)){ targetObj[keys]=source[keys]; } } returntargetObj; }
我们来测试一下:
constoriginObj={a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}}; constcloneObj=shallowClone(originObj); console.log(cloneObj===originObj);//false cloneObj.a='aa'; cloneObj.c=[1,1,1]; cloneObj.d.dd='surprise';
经过上面的修改,cloneObj不用说,肯定是{a:'aa',b:'b',c:[1,1,1],d:{dd:'surprise'}}了,那originObj呢?刚刚我们验证了cloneObj===originObj是false,说明这两个对象引用地址不同啊,那应该就是修改了cloneObj并不影响originObj。
console.log(cloneObj);//{a:'aa',b:'b',c:[1,1,1],d:{dd:'surprise'}} console.log(originObj);//{a:'a',b:'b',c:[1,2,3],d:{dd:'surprise'}}
Whathappend?
originObj 中关于 a、c都没被影响,但是 d 中的一个对象被修改了。。。说好的深拷贝呢?不是引用地址都不一样了吗?
原来是这样:
- 从 shallowClone 的代码中我们可以看出,我们只对第一层的目标进行了 深拷贝 ,而第二层开始的目标我们是直接利用 = 赋值操作符进行拷贝的。
- so,第二层后的目标都只是复制了一个引用,也就是浅拷贝。
总结
- 赋值运算符 = 实现的是浅拷贝,只拷贝对象的引用值;
- JavaScript中数组和对象自带的拷贝方法都是“首层浅拷贝”;
- JSON.stringify 实现的是深拷贝,但是对目标对象有要求;
- 若想真正意义上的深拷贝,请递归。