浅拷贝和深拷贝

江枫思渺然 提交于 2019-11-28 06:23:59

  在介绍浅拷贝和深拷贝的区别之前,先看一个例子,或许可以方便我们理解:

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

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!