在了解深浅拷贝之前,需要先明白一个概念:堆和栈
堆和栈
堆和栈都是内存中划分出来用来存储的区域。栈(stack)为自动分配的内存空间,它由系统自动释放;而堆(heap)则是动态分配的内存,大小不定也不会自动释放。
JS中的数据类型
基本数据类型存放在栈中
基本数据类型:undefined,boolean,number,string,null
var num = 123;
var str="abc";
以上数据在栈中的存储形式如下图:
引用类型的数据存放在堆中
JS中除了基本数据类型以外的都是对象,如对象、数组、函数等,当声明这些数据时,其变量名和堆地址存放在栈中,而数据存放在堆中。
var obj = {name: 'xiaoming'};
var arr = [1,2,3];
以上数据在堆和栈中的存储形式如下图:
传值与传址
了解了基本数据类型与引用类型的区别之后,我们就应该能明白传值与传址的区别了。
在我们进行赋值操作的时候,基本数据类型的赋值(=)是在内存中新开辟一段栈内存,然后再把再将值赋值到新的栈中。例如:
var a = 123;
var b = a;
a++;
console.log(a); //124
console.log(b); //123
以上代码在栈中的过程如下图:
由此可见,对于基本数据类型的赋值,两个变量是两个独立相互不影响的变量,改变其中一个并不会影响另一个的值。
而引用数据类型的赋值是传址。也就是赋值后取到的是其堆内存地址,所以最终两个变量会指向同一个堆内存地址,因此两者之间操作互相有影响。
var obj1 = {name: 'zhangsan'};
var obj2 = obj1;
obj2.name = 'lisi';
console.log(obj1.name); // lisi
console.log(obj2.name); // lisi
当我们改变obj2.name的值时,obj1.name的值也同时被修改。
此时我们假设obj1指向的堆内存地址为x0011,下面我们已图的形式看看这个过程。
由此可见,当我们将obj1赋值给obj2时,实际上只是把obj1中存储的堆内存地址(x0011)赋给了obj2,实际上obj1和obj2指向的是同一片堆空间,因此当改变obj2.name时,obj1也会一同改变。
赋值VS浅拷贝
那么赋值和浅拷贝有什么区别呢,我们来一个例子:
var obj1 = {
name: 'zhangsan',
age: 18,
arr: [1, 2, 3]
}
var obj2 = obj1 //赋值
var obj3 = shallowCopy(obj1) //浅拷贝
function shallowCopy(dest={}, obj){
for (var prop in obj) {
if (obj.hasOwnProperty(prop)) {
dest[prop] = obj[prop];
}
}
return dest;
}
//改变obj2
obj2.name = 'lisi';
obj2.age = 20;
obj2.arr = [3, 4, 5];
console.log(obj1);
//obj1 = {
// name: 'lisi',
// age: 20,
// arr: [3, 4, 5]
// }
//改变obj3
obj3.name = 'wangwu';
obj3.age = 22;
obj3.arr = [6, 7, 8];
console.log(obj1);
//注意,此时obj1已经跟随obj2改变过了
//obj1 = {
// name: 'lisi',
// age: 20,
// arr: [6, 7, 8]
// }
console.log(obj3);
//obj1 = {
// name: 'wangwu',
// age: 22,
// arr: [6, 7, 8]
// }
通过上面的例子,我们不难看出来,通过赋值得来的obj2,在改变obj2中的任何值时(包括基本数据类型和引用类型),obj1都会跟着改变。
而通过浅拷贝得到的obj3,在修改obj3中的基本数据类型的值时,obj1不会受到影响,而修改obj3中的引用类型数据时,obj1同样会跟着改变。我们可以把obj1中的基本数据类型看作第一层,引用数据类型看成第二层。也就是说通过浅拷贝获得的对象,在改变其第一层数据时并不会影响到原对象中的数据,而改变其第二层数据时,原对象仍然会受到影响。
而第二层会跟着改变的原因,仍然是其中的引用数据类型在栈中存储的是堆内存地址造成的。
那要怎么样才能使在改变第二层甚至第n层的时候,原对象不会受到影响呢?这就是深拷贝的作用。
深拷贝
深拷贝是对对象以及对象的所有子对象进行拷贝。
那么问题来了,怎么进行深拷贝呢?
思路就是递归调用刚刚的浅拷贝,把所有属于对象的属性类型都遍历赋给另一个对象即可。由于本文旨在讲清楚赋值、浅拷贝、深拷贝的概念及区别,所以下面的深拷贝函数是简化版,只考虑子对象为数组或者对象类型。
var Person = {
name: 'zhangsan',
age: 18,
other: {
str: 'abc',
arr: [1, 2, 3]
}
}
function deepCopy(dest={}, obj){
//遍历原对象
for(var key in obj){
//判断数据是否为引用类型
if(typeof obj[key] === 'object'){
//判断数据为数组类型还是对象类型,并进行初始化
dest[key] = Object.prototype.toString.call(obj) == {} ? {} : [];
//递归
deepCopy(dest[key], obj[key])
}else {
//非引用类型则直接赋值
dest[key] = obj[key];
}
}
return dest;
}
var obj2 = deepCopy(dest={}, Person)
obj2.other.str = 'xyz'
obj2.other.arr = [4 ,5, 6]
console.log(obj1.other.str) //'abc'
console.log(obj1.other.arr) //[1, 2, 3]
console.log(obj2.other.str) //'xyz'
console.log(obj2.other.arr) //[4 ,5, 6]
总结:
– | 和原数据是否指向同一对象 | 第一层数据为基本数据类型 | 原数据中包含子对象 |
---|---|---|---|
赋值 | 是 | 改变会使原数据一同改变 | 改变会使原数据一同改变 |
浅拷贝 | 否 | 改变不会使原数据一同改变 | 改变会使原数据一同改变 |
深拷贝 | 否 | 改变不会使原数据一同改变 | 改变不会使原数据一同改变 |