在介绍浅拷贝和深拷贝的区别之前,先看一个例子,或许可以方便我们理解:
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()将该字符串转换回对象,也就是重新分配一块空间给这个对象,所以拷贝出来的对象与原先对象互不影响。虽然这种方法很方便,但是投机取巧总归来说也不是太好,还是建议大家要自己会写使用遍历+递归的方法来进行深拷贝。