深浅拷贝详解

拷贝的基本定义

  一般而言,深浅拷贝主要用来形容JavaScript中,关于对象的复制的。特别值得注意的一点就是,数组在JavaScript中的类型是属于Object。

  浅拷贝即只复制对象的引用,所以副本最终也是指向父对象在堆内存中的对象,无论是副本还是父对象修改了这个对象,副本或者父对象都会因此发生同样的改变;

  而深拷贝则是直接复制父对象在堆内存中的对象,最终在堆内存中生成一个独立的,与父对象无关的新对象。深拷贝的对象虽然与父对象无关,但是却与父对象一致。当深拷贝完成之后,如果对父对象进行了改变,不会影响到深拷贝的副本,同理反之亦然。

数组的浅拷贝

  关于数组的浅拷贝,首先我们需要唠嗑一下Array类提供的API:concat、slice;这两个方法都会返回一个新数组,所以很多人一开始都误以为这是属于深拷贝的,其实不然。

  MDN中关于concat的描述非常清楚:concat方法不会改变this或任何作为参数提供的数组,而是返回一个浅拷贝,它包含与原始数组相结合的相同元素的副本。具体请看下面案例:

强调:

  下面的例中,我们一直都会使用复杂类型来称呼,并且在案例中,复杂类型其实我们都是使用了JS中的对象,此时有基本知识不牢固的同学就会纳闷,为什么不直接说对象。请注意!JS中的复杂类型有两种,一种是Object,一种是Array;实质上function也是复杂类型的一种,但是实质上Array和function都属于Object!据此,在数组中如果元素是数组,那么跟元素是对象,在拷贝时都是一样的,下面只展示了是对象时的案例,是碍于篇幅的问题,请各位转换思路即可。

concat方法

当数组内的元素都是简单类型时:

1 var arr1 = [0,1,2,3,4,5];
2 var arr2 = [6,7,8,9,10];
3 var arr = arr1.concat(arr2);
4 // 更改arr1 中索引位 0 的元素的值
5 arr1[0] = 5;
6 console.log(arr);//输出结果: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

  可以看到,这种情况下活生生就是一个深拷贝的存在,别着急,我们继续看下面的案例。

当数组内的元素有复杂类型时:

1 var arr3 = [{name:'xiaobai',age:18},10];
2 var arr4 = [1,2,3];
3 arr5 = arr3.concat(arr4);
4 //更改arr3 中索引值为 0 的对象的属性
5 arr3[0].name = '小白';
6 console.log(arr5);//输出结果:[{name:'小白',age:18}, 10, 1, 2, 3]

  此时就很清晰地可以看到,当数组内的对象发生改变的时候,使用concat合并的新数组同样发生了改变。其实这并不难理解。

  深浅拷贝一般都是用于描述复杂类型的复制的,基本类型的复制是在栈内存中生成一个一模一样的值,父本和副本之间互相没有任何关系。而复杂类型由于是通过在栈内存中的引用指向堆内存中的对象,所以根据复制栈内存的引用和复制堆内存的对象区分深浅拷贝。

slice方法

  同样的道理,我们在看看slice()方法,根据MDN的描述:slice() 方法返回一个新的数组对象,这一对象是一个由 begin 和 end 决定的原数组的浅拷贝(包括 begin,不包括end)。原始数组不会被改变。

  同样的,MDN中也描述了,如果元素是一个对象引用(不是实际对象),只会返回一个浅复制的新数组。我们通过案例来学习:

当数组内的元素是简单类型时:

1         var arr1 = [0,1,2,3,4,5];
2         var arr = arr1.slice();
3         // 更改arr1 中索引位 0 的元素的值
4         arr1[0] = 5;
5         console.log(arr);//输出结果: [0, 1, 2, 3, 4, 5]

  这是因为对于字符串、数字及布尔值来说(不是 StringNumber 或者 Boolean 对象),slice 会拷贝这些值到新的数组里。在别的数组里修改这些字符串或数字或是布尔值,将不会影响另一个数组。

当数组内的元素是复杂类型时:

1         var arr3 = [{name:'xiaobai',age:18},10];
2         arr5 = arr3.slice();
3         //更改arr3 中索引值为 0 的对象的属性
4         arr3[0].name = '小白';
5         console.log(arr5);//输出结果:[{name:'小白',age:18}]

  这是因为如果该元素是个对象引用 (不是实际的对象),slice 会拷贝这个对象引用到新的数组里。两个对象引用都引用了同一个对象。如果被引用的对象发生改变,则新的和原来的数组中的这个元素也会发生改变。

使用等号(=)实现数组的浅拷贝

  使用等号实现数组的浅拷贝就不再过多言语了,如果数组的元素是基本数据类型,那么会直接克隆该元素到新数组,如果元素是复杂类型时,克隆的则是复杂类型的指向,这种情况下,无论是新数组还是旧数组,改变了复杂类型,两个数组都会受到一样的影响。

数组/对象的深拷贝

  js数组中实现深拷贝的方法有很多中,比如JSON.parse(JSON.stringify())、递归以及jQuery库的extend方法,都是可以实现数组和对象的深拷贝的。

  但是仔细去品味,你会发现实质数组的深拷贝也是针对的复杂类型而言的,所以我们实现了数组的深拷贝,就意味着我们同时也可以实现复杂类型的深拷贝!据此我们同时把数组和对象的深拷贝放在一起演示。

JSON.parse(JSON.stringify())深拷贝

  首先我们看一下,这种方式下复制的对象之间是一种怎样的关系:

1 var obj = {
2     name: 'xiaobai',
3     age: 18
4     }
5 
6 var copy = JSON.parse(JSON.stringify(obj));
7 console.log(obj);//输出结果:{name: "xiaobai", age: 18}
8 console.log(copy);//输出结果:{name: "xiaobai", age: 18}
9 console.log(obj === copy);//输出结果:false

  可以看到,对于复杂类型的对象,使用JSON先序列化然后再反序列化,得到的结果虽然一致,但是变量 obj 和 copy 所指向的对象不是同一个,他们之间的比较结果是false,这种情况下两个对象之一发生的任何变化都不会影响到其他一个。此时我们就实现了复杂类型的深拷贝了!但是千万注意!JSON.Stringtify这个方法不能深拷贝函数!!这个要记得!

  基于数组的深拷贝其实就是针对数组中的复杂类型的深拷贝,所以我们对整个数组使用JSON.parse(JSON.stringify())深拷贝即可。

递归深拷贝

  在使用for...in 来实现深拷贝的时候需要注意一个坑,即原型链上的可枚举属性会被遍历出来,这是我们所不希望看到的,所以进入正题之前首先说一下如何避免遍历到原型链上的可枚举属性:

 1 var obj = {
 2     name: 'xiaohei',
 3     age: 18
 4 }
 5 //在原型链上添加一个自定义的可枚举属性
 6 obj.__proto__.eat = 'hello';
 7 
 8 for(var key in obj){
 9     console.log(key);
10     //遍历结果:name    age     eat
11 }

  可以看到,原型链上的可枚举属性也被遍历出来了,为了避免出现这样的情况,我们可以使用js提供的一个方法hasOwnProperty(),该方法会判断当前属性是自身属性还是继承而来的,或者说是自身属性还是原型链上的可枚举属性,返回值使boolean,自身属性为true,原型链可枚举属性为false。

 1 var obj = {
 2     name: 'xiaohei',
 3     age: 18
 4 }
 5 //在原型链上添加一个自定义的可枚举属性
 6 obj.__proto__.eat = 'hello';
 7 
 8 for(var key in obj){
 9     if(obj.hasOwnProperty(key)){
10         console.log(key);
11         //输出结果: name    age
12     }
13 }

  OK,进入正题,编写一个递归方法,用来深拷贝复杂类型:

 1
/**
 2  * 
 3  * @param {Object} obj 传递进去进行递归的数组或者对象
 4  */
 5 function deepClone(obj){
            if(obj === null) return null;
            if(typeof obj !== 'object') return obj;
            if(obj instanceof RegExp) {
                return new RegExp(obj);
            }
            if(obj instanceof Date) {
                return new Date(obj);
            }
            /* 这里之所以使用constructor,是为了可以使的克隆出来的对象
            与被克隆的对象都是同一个类的实例对象 */
            let newObj = new obj.constructor;
            for(let key in obj) {
                if(obj.hosOwnProperty(key)) {
                    newObj[key] = deepClone(obj[key]);
                }
            }
            return newObj;
        }
24 
25 var arr = [1, 2, { name: 'xiaohei', age: 18 }];
26 var newArr = deepClone(arr);
27 //修改父数组中对象元素的属性值
28 arr[2].name = '小黑';
29 console.log(arr);
30 console.log(newArr);
31 console.log(arr === newArr);


 

  最终输出的结果如下:

  

原文地址:https://www.cnblogs.com/littleppig/p/13443981.html