javascript中的深复制和浅复制

2017-09-10

在明白什么深复制和浅复制之前,首先明确几个概念。

栈(stack)和堆(heap)

上的空间是自动分配自动回收的,所以栈上的数据的生存周期只是在函数的运行过程中,运行后就释放掉,不可以再访问。

则是程序员根据需要自己申请的空间,大小不定也不会自动释放。只要程序员不释放空间,就一直可以访问到,一旦忘记释放会造成内存泄露。

基本类型和引用类型

基本类型:存放在栈内存中的简单数据段,数据大小确定,内存空间大小可以分配。基本数据类型(null,undefined,string,number和boolean)是直接按值存放的,所以可以直接访问。

引用类型:存放在堆内存中的对象,变量实际保存的是一个指针,这个指针指向另一个位置。每个空间大小不一样。
当我们需要访问引用类型(如对象,数组,函数等)的值时,首先从栈中获得该对象的地址指针,然后再从堆内存中取得所需的数据。

浅复制 vs 深复制

浅复制和深复制都可以实现在已有对象的基础上再生一份的作用,但是对象的实例是存储在堆内存中然后通过一个引用值去操作对象,由此复制的时候就存在两种情况了:复制引用和复制实例,这也是浅复制和深复制的区别所在。

浅复制:浅复制是复制引用,复制后的引用都是指向同一个对象的实例,彼此之间的操作会互相影响

深复制:深复制不是简单的复制引用,而是在堆中重新分配内存,并且把源对象实例的所有属性都进行新建复制,以保证深复制的对象的引用图不包含任何原有对象或对象图上的任何对象,复制后的对象与原来的对象是完全隔离的。

浅复制

Array的slice和concat方法

Array的slice和concat方法都会返回一个新的数组实例,但是这两个方法对于数组中的对象元素却没有执行深复制,而只是复制了引用的浅复制,通过以下代码进行理解:

1
2
3
4
5
6
7
var array = [1,2,3];
var array_shallow = array;
var array_concat = array.concat();
var array_slice = array.slice(0);
console.log(array === array_shallow); //true
console.log(array === array_slice); //false
console.log(array === array_concat); //false

可以看出,concat和slice返回的不同的数组实例,这与直接的引用复制是不同的。

1
2
3
4
5
6
7
8
9
10
11
var array = [1, [1,2,3], {name:"array"}];
var array_concat = array.concat();
var array_slice = array.slice(0);
//改变array_concat中数组元素的值
array_concat[1][0] = 5;
console.log(array[1]); //[5,2,3]
console.log(array_slice[1]); //[5,2,3]
//改变array_slice中对象元素的值
array_slice[2].name = "array_slice";
console.log(array[2].name); //array_slice
console.log(array_concat[2].name); //array_slice

通过代码的输出可以看出concat和slice并不是真正的深复制,数组中的对象元素(Object,Array等)只是复制了引用。

深复制

JSON对象的parse和stringify

JSON对象parse方法可以将JSON字符串反序列化成JS对象,stringify方法可以将JS对象序列化成JSON字符串,借助这两个方法,也可以实现对象的深复制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var source = {
name:"source",
child:{
name:"child"
}
}
var target = JSON.parse(JSON.stringify(source));
//改变target的name属性
target.name = "target";
console.log(source.name); //source
console.log(target.name); //target
//改变target的child
target.child.name = "target child";
console.log(source.child.name); //child
console.log(target.child.name); //target child

从代码的输出可以看出,复制后的target与source是完全隔离的,二者不会相互影响。

这个方法使用较为简单,可以满足基本的深复制需求,而且能够处理JSON格式能表示的所有数据类型,但是对于正则表达式类型、函数类型等无法进行深复制(而且会直接丢失相应的值),同时如果对象中存在循环引用的情况也无法正确处理。

自己实现一个深复制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function getType(obj) {
return Object.prototype.toString.call(obj).slice(8,-1);
}

function deepClone(obj){
var type = getType(obj);
var result;
if (type === "Array") {
result = [];
}else if(type === "Object"){
result = {};
}else{
//除了数组和对象,其他基本类型的数据都可以通过简单赋值进行克隆。
return obj;
};
//对象属性的遍历
for (var key in obj) {
//继续判断属性的数据类型
if (getType(obj[key]) == "Object" || getType(obj[key])=="Array") {
//如果属性值是对象,递归调用
result[key] = deepClone(obj[key]);
} else{
//如果属性都是简单的数据段,直接赋值
result[key] = obj[key];
}
}
return result;
}