我们知道JavaScript的数据类型分为基本数据类型和引用数据类型。对于基本数据类型,不存在深浅拷贝的问题,只要是复制,都相当于是深拷贝。而对于引用数据类型,一般的拷贝只是做到了引用地址的拷贝,而不是深层次的拷贝,所以下面我们所有的讨论都是基于引用数据类型的。
浅拷贝
浅拷贝很简单,=的操作就是。
const originArray = [1,2,3,4,5];const originObj = {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};const cloneArray = originArray;const cloneObj = 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'}}
深拷贝
深拷贝就是对目标的完全拷贝,不只是对象引用额拷贝,而是值的拷贝。
实现了深拷贝就使两个对象完全无关,各自的变化不会相互影响。
实现深拷贝的方法有
- 利用JSON对象的parse和stringify
- 利用递归实现每一层重新创建对象赋值
JSON.stringify/parse的方法
这个JSON方法能实现原理很简单,stringify相当于是把对象变成了字符串,而字符串是基本数据类型,=号的操作就是深拷贝了,再利用parse重新解析为对象。 ```javascript const originArray = [1,2,3,4,5]; const cloneArray = JSON.parse(JSON.stringify(originArray)); console.log(cloneArray === originArray); // false
const originObj = {a:’a’,b:’b’,c:[1,2,3],d:{dd:’dd’}}; const cloneObj = 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’}};
很容易我们就实现了深拷贝,但是越简单我们越要警惕,为什么这个这么简单,还要去研究其他的方法呢?下面我们就来看一种情况```javascriptconst originObj = {name:'axuebin',sayHello:function(){console.log('Hello World');}}console.log(originObj); // {name: "axuebin", sayHello: ƒ}const cloneObj = JSON.parse(JSON.stringify(originObj));console.log(cloneObj); // {name: "axuebin"}
元对象的sayHello是一个函数,对于函数,这个方法不回去转化,mdn上有一段话解释了这个问题,大概意思就是:
undefined,function,symbol会在转换过程中被忽略。
所以,虽然很简单的实现了深拷贝,但是缺点也是很明显的,所以我们需要另一种方法来完美的实现深拷贝。
递归实现深拷贝
递归的思路很简单,就是遍历对象,如果值是一个基本数据类型,那么就直接拷贝,如果是一个引用数据类型,那么就利用递归拷贝,知道遍历所有值都是基本数据类型。
function deepClone(source){const targetObj = source.constructor === Array ? [] : {}; // 判断复制的目标是数组还是对象for(let keys in source){ // 遍历目标if(source.hasOwnProperty(keys)){if(source[keys] && typeof source[keys] === 'object'){ // 如果值是对象,就递归一下targetObj[keys] = source[keys].constructor === Array ? [] : {};targetObj[keys] = deepClone(source[keys]);}else{ // 如果不是,就直接赋值targetObj[keys] = source[keys];}}}return targetObj;}const originObj = {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};const cloneObj = deepClone(originObj);console.log(cloneObj === originObj); // falsecloneObj.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'}};const originObj = {name:'axuebin',sayHello:function(){console.log('Hello World');}}console.log(originObj); // {name: "axuebin", sayHello: ƒ}const cloneObj = deepClone(originObj);console.log(cloneObj); // {name: "axuebin", sayHello: ƒ}
搞定
JavaScript中的拷贝方法
在JavaScript中,我们经常会使用到一下方法,去进行复制对象的操作。比如数组的concat和slice方法,这两个方法不会修改原数组,而是返回一个新的数组。
另外还有es6的Object.assign和…展开运算符
它们是不是深拷贝的,很简单,试试就知道了。
//concatconst originArray = [1,2,3,4,5];const cloneArray = originArray.concat();console.log(cloneArray === originArray); // falsecloneArray.push(6); // [1,2,3,4,5,6]console.log(originArray); [1,2,3,4,5];const originArray = [1,2,3,4,5];const cloneArray = originArray.concat();console.log(cloneArray === originArray); // falsecloneArray.push(6); // [1,2,3,4,5,6]console.log(originArray); [1,2,3,4,5];
结果很明显,对于第一层我们进行了深拷贝,而如果第二层第三层仍然是引用数据类型,那么我们得到的还是一个引用。
//sliceconst originArray = [1,2,3,4,5];const cloneArray = originArray.slice();console.log(cloneArray === originArray); // falsecloneArray.push(6); // [1,2,3,4,5,6]console.log(originArray); [1,2,3,4,5];const originArray = [1,[1,2,3],{a:1}];const cloneArray = originArray.slice();console.log(cloneArray === originArray); // falsecloneArray[1].push(4);cloneArray[2].a = 2;console.log(originArray); // [1,[1,2,3,4],{a:2}]//Object.assignlet obj = {a: 1, b: 2}let newObj = Object.assign({}, obj)// console.log(obj, newObj)newObj.a = 3console.log(obj, newObj) //{ a: 1, b: 2 } { a: 3, b: 2 }let obj2 = {a: 1, b: 2, c: {cc: 'cc'}}let newObj2 = Object.assign({}, obj2)newObj2.c.cc = 'ccc'console.log(obj2, newObj2)//{ a: 1, b: 2, c: { cc: 'ccc' } } { a: 1, b: 2, c: { cc: 'ccc' } }//...展开运算符const originArray = [1,2,3,4,5,[6,7,8]];const originObj = {a:1,b:{bb:1}};const cloneArray = [...originArray];cloneArray[0] = 0;cloneArray[5].push(9);console.log(originArray); // [1,2,3,4,5,[6,7,8,9]]const cloneObj = {...originObj};cloneObj.a = 2;cloneObj.b.bb = 2;console.log(originObj); // {a:1,b:{bb:2}}
最终,我们得到的结果都一样,这些方法只是对第一层进行了深拷贝,如果第二层仍然是引用数据类型,就只是拷贝的值的引用。
总结
- 赋值运算符=实现的浅拷贝,只能拷贝对象的引用值
- JavaScript中数组和对象自带的拷贝方法都是首层浅拷贝
- JSON.stringify实现的是深拷贝,但是对目标有要求
- 递归可以实现真正意义的深拷贝
