在介绍浅拷贝和深拷贝的区别之前,先看一个例子,或许可以方便我们理解:
1 /* 2 ** example 1 3 */ 4 let value_1 = 1; 5 let value_2 = value_1; 6 7 value_2 = 2; 8 console.log(value_1); // 1 9 console.log(value_2); // 2 10 11 /* 12 ** example 2 13 */ 14 let value_3 = { 15 name: "Peter", 16 age: 22 17 }; 18 let value_4 = value_3; 19 20 value_4.name = "May"; 21 console.log(value_3.name); // May 22 console.log(value_4.name); // May 23 24 /* 25 ** example 3 26 */ 27 let value_5 = { 28 name: "Peter", 29 age: 22 30 }; 31 let value_6 = { 32 name: value_5.name, 33 age: value_5.age 34 }; 35 36 value_6.name = "May"; 37 console.log(value_5.name); // Peter 38 console.log(value_6.name); // May
其中,example 1 和 example 2 就是我们平时用得最多的拷贝,也就是浅拷贝。
ps:由于浅拷贝和深拷贝一般都是针对于对象以及数组而言的,example 1 只用于对比。
通过上面的 example 2 我们可以看到,如果我们是直接将一个数组/对象赋值给另外一个变量,当我们对其中一个变量的值进行修改的时候,另外一个的值也会随之改变,究其原因其实就是在我们进行赋值的时候,实际上是将变量1的引用,赋值给了变量2,换句话说,实际上我们是将变量1的地址赋值给了变量2,所以才会出现上面的情况,这也就是我们常说的浅拷贝。
我们先来看一下浅拷贝、深拷贝的定义:
- 浅拷贝: 将原对象或原数组的引用直接赋给新对象、新数组,新对象/数组只是原对象的一个引用;
- 深拷贝: 创建一个新的对象和数组,将原对象的各项属性的“值”(数组的所有元素)拷贝过来,是“值”而不是“引用”。
所以当我们希望复制一个对象/数组的值,又不希望对其造成修改时,就需要用到深拷贝了。可以看一下上面的 example 3 ,就是一个十分简单的深拷贝写法,但是这种写法十分具有局限性,当对象/数组中的元素足够多的时候,我们还能用这种方法来进行深拷贝吗?答案很明显是不能的。聪明的你肯定想到了,“那我可以对原对象/数组进行遍历,再将其中每一个值赋值给新变量啊!”。那我们抱着实验出真知的态度,来看一下下面这段代码:
1 function DeepCopy(data) { 2 // 当参数为空 或者 参数不是对象的时候,直接返回其本身 3 if (!data || data instanceof Object == false) return data; 4 5 // 当传入的参数为一个数组时 6 if (data instanceof Array) { 7 let newData = []; 8 for (let i of data) { 9 newData.push(i); 10 } 11 return newData; 12 } else { 13 let newData = {}; 14 for (let i in data) { 15 newData[i] = data[i]; 16 } 17 return newData; 18 } 19 } 20 21 let array1 = [1, 2, 3, 4]; 22 let array2 = DeepCopy(array1); 23 console.log(array1); // [1, 2, 3, 4] 24 console.log(array2); // [1, 2, 3, 4] 25 array2[2] = 5; 26 console.log(array1); // [1, 2, 3, 4] 27 console.log(array2); // [1, 2, 5, 4] 28 29 let object1 = { 30 name: "Peter", 31 age: 22 32 }; 33 let object2 = DeepCopy(object1); 34 console.log(object1); // {name: "Peter", age: 22} 35 console.log(object2); // {name: "Peter", age: 22} 36 object2.name = "Lily"; 37 console.log(object1); // {name: "Peter", age: 22} 38 console.log(object2); // {name: "Lily", age: 22}
上面的代码乍一看,似乎没什么问题,而且也可以达到我们深拷贝的效果,但是如果将测试集换成以下代码再来验证以下,似乎又会出现不同的结果:
1 let array3 = [1, [2, 3], 4]; 2 let array4 = DeepCopy(array3); 3 array4[1][1] = 5; 4 console.log(array3); // [1, [2, 5], 4] 5 console.log(array4); // [1, [2, 5], 4]
通过上面的测试我们可以看到,当原数组/对象中还包含着数组/对象时,只用单独一层拷贝是完全不够的,原因我们上面已经讲过了,拷贝过去的只是它的地址,所以当其中一个变量发生改变时另外一个也会跟着改变。那我们尝试来变通一下,当我们在遍历数组/对象的子项时,当发现其子项是数组/对象时,也对它进行一次深拷贝不就可以了吗。我们来尝试一下,对上面的代码稍作修改:
function DeepCopy(data) { // 当参数为空 或者 参数不是对象的时候,直接返回其本身 if (!data || data instanceof Object == false) return data; // 当传入的参数为一个数组时 if (data instanceof Array) { let newData = []; for (let i of data) { if (i instanceof Object) { newData.push(DeepCopy(i)); } else { newData.push(i); } } return newData; } else { let newData = {}; for (let i in data) { if (data[i] instanceof Object) { newData[i] = DeepCopy(data[i]); } else { newData[i] = data[i]; } } return newData; } } let array1 = [1, [2, [3, [4, [5, 6, {a: 7, b: 8}]]]]]; let array2 = DeepCopy(array1); console.log(array1); // [1, [2, [3, [4, [5, 6, {a: 7, b: 8}]]]]] console.log(array2); // [1, [2, [3, [4, [5, 6, {a: 7, b: 8}]]]]] array2[1][1][1][1][2].a = 10; array2[1][0] = 10; console.log(array1); // [1, [2, [3, [4, [5, 6, {a: 7, b: 8}]]]]] console.log(array2); // [1, [10, [3, [4, [5, 6, {a: 10, b: 8}]]]]]
修改之后,无论数组/对象中镶嵌着多少层子数组/对象,我们都可以将其完整的深拷贝下来。深拷贝在实际开发环境中用到的情况也很多,比如说我自己开发过的,从服务器获取到数据之后,有时候可能需要对数据进行处理之后再进行显示,但我又需要保留原有数据进行比较,深拷贝在这种情况下遍会发挥很重要的作用了。另外,浅拷贝和深拷贝也是前端面试中的常考题,需要加深对这两种拷贝方式的理解。
最后,分享一些在别的博主那里看到的,使用一些“小技巧”来进行深拷贝。
方法一:使用slice()来进行深拷贝。
我们都知道,在js中slice(start, end)是用来对数组进行切割,当不传入任何参数时,默认返回一个长度和原数组相同的新数组,也就是所谓的深拷贝。但是这种方法只适用于对一维数组进行深拷贝,对对象(因为对象没有slice()方法)以及多维数组(理由同上面讲的一样)无效。
方法二:使用concat()方法来进行深拷贝。
concat(arr1, arr2, ..., arrn)方法是用来连接多个数组,但是该方法不会改变现有的数组,而是只会返回被连接数组的一个副本,所以也可以用来对一维数组进行深拷贝。但是弊端也很明显,跟上面方法一所说的是一样。
方法三:Object.assign()
在ES6中,提供了Object.assign(target, source1, source2)这个方法来进行深拷贝。它主要是用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target),并返回合并后的target。所以我们可以使用 copyObj = Object.assign({}, obj) 这段代码来将obj中的内容深拷贝到copyObj中,这段代码将会把obj中的一级属性都拷贝到 {}中,然后将其返回赋给copyObj。但是这个方法也是只能用来深拷贝只有一级的对象,当对象的子项中含有数组/对象时,这种拷贝方式也会失败。
总结一下,虽然上面所说的三种方法用起来都十分方便,但是使用也十分具有局限性,只能用于一维数组或只有一级的对象。
但是呢,其实还有一种很简单的方法,可以对多维数组以及多级对象进行深拷贝,那就是使用JSON.stringify()和JSON.parse()。这个方法的原理很简单,先使用JSON.stringify()将数组/对象转换成字符串,再使用JSON.parse()将该字符串转换回对象,也就是重新分配一块空间给这个对象,所以拷贝出来的对象与原先对象互不影响。虽然这种方法很方便,但是投机取巧总归来说也不是太好,还是建议大家要自己会写使用遍历+递归的方法来进行深拷贝。