javascript中浅拷贝和深拷贝

javascript的变量类型

  1. 基本类型:存放在栈内存中的简单数据段,数据大小确定,内存空间大小可以分配。
    5种基本数据类型有Undefined、Null、Boolean、Number 和 String,它们是直接按值存放的,所以可以直接访问。

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

javascript存储对象都是存地址的,所以浅拷贝会导致 arr1 和arr2 指向同一块内存地址。改变了其中一方的内容,都是在原来的内存上做修改会导致拷贝对象和源对象都发生改变,而深拷贝是开辟一块新的内存地址,将原对象的各个属性逐个复制进去。对拷贝对象和源对象各自的操作互不影响。

例如:数组拷贝

1
2
3
4
5
6
//浅拷贝,双向改变,指向同一片内存空间
var arr1 = [1, 2, 3];
var arr2 = arr1;
arr1[0] = '2';
console.log(arr1); //2,2,3
console.log(arr2); //2,2,3

浅拷贝

简单的引用复制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var a = {
key1: "11111"
}

function Copy(p) {
var c = {};
for (var i in p) { 
c[i] = p[i];
}
return c;
}
a.key2 = ['小辉', '小辉'];
var b = Copy(a);
b.key2.push("大辉");
console.log(b.key2); //小辉,小辉,大辉
console.log(a.key2); //小辉,小辉,大辉

a对象中key1属性是字符串,key2属性是数组。a拷贝到b,key1、key2属性均顺利拷贝。给b对象的key2属性进行修改时,a对象的key2属性也会随之改变。说明a,b对象上的key2属性指向的是同一个内存地址(x1),当对b对象的key2属性进行修改时,实际上是修改的x1指向的内存空间。

1
2
3
b.key1 = 1;
console.log(b.key1); //1
console.log(1.key1); //11111

给b对象的key1属性进行修改时,a对象的key1属性不会改变。这是由于a,b对象的key1属性都是基本数据类型,会直接存储在内存中,b对象的key1的修改不会对a对象的key1产生影响。其在内存的状态,可以用下图来表示。

深拷贝

递归遍历
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function deepCopy(p, c) {
var c = c || {};
for (var i in p) {
if (typeof p[i] === 'object') {  
c[i] = (p[i].constructor === Array) ? [] : {};
deepCopy(p[i], c[i]);  
} else {     
c[i] = p[i];  
}
}
return c;  
}
a.key2 = ['小辉', '小辉'];
var b = {};
b = deepCopy(a, b);
b.key2.push("大辉");
alert(b.key2); //小辉,小辉,大辉
alert(a.key2); //小辉,小辉

由上可知,修改b的key2数组时,没有使a父对象中的key2数组新增一个值,即子对象没有影响到父对象a中的key2。其存储模式大致如下:

Array的slice和concat方法

Array的slice和concat方法不修改原数组,只会返回一个浅复制了原数组中的元素的一个新数组。之所以把它放在深拷贝里,是因为它看起来像是深拷贝。而实际上它是浅拷贝。原数组的元素会按照下述规则拷贝:

  • 如果该元素是个对象引用 (不是实际的对象),slice 会拷贝这个对象引用到新的数组里。两个对象引用都引用了同一个对象。如果被引用的对象发生改变,则新的和原来的数组中的这个元素也会发生改变。
  • 对于字符串、数字及布尔值来说(不是 String、Number 或者 Boolean 对象),slice 会拷贝这些值到新的数组里。在别的数组里修改这些字符串或数字或是布尔值,将不会影响另一个数组。

例子如下:

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返回的不同的数组实例,这与直接的引用复制是不同的。而从另一个例子可以看出Array的concat和slice并不是真正的深复制,数组中的对象元素(Object,Array等)只是复制了引用。如下:

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

JSON对象是ES5中引入的新的类型(支持的浏览器为IE8+),JSON对象parse方法可以将JSON字符串反序列化成JS对象,stringify方法可以将JS对象序列化成JSON字符串,借助这两个方法,也可以实现对象的深拷贝。

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

//例2
var source = { name:function(){console.log(1);}, child:{ name:"child" } }
var target = JSON.parse(JSON.stringify(source));
console.log(target.name); //undefined

//例3
var source = { name:function(){console.log(1);}, child:new RegExp("e") }
var target = JSON.parse(JSON.stringify(source));
console.log(target.name); //undefined
console.log(target.child); //Object {}

这种方法使用较为简单,可以满足基本的深拷贝需求,而且能够处理JSON格式能表示的所有数据类型,但是对于正则表达式类型、函数类型等无法进行深拷贝(而且会直接丢失相应的值)。