js中的浅拷贝与深拷贝

在js中,我们经常复制一个对象,复制数据,今天来总结一下js中的深浅拷贝问题。

js中的内存

js中的存储分为堆内存栈内存

基本类型有Undefined、Null、Boolean、Number 和String。这些类型在内存中分别占有固定大小的空间,他们的值保存在栈空间,我们通过按值来访问的。

引用类型,值大小不固定,栈内存中存放地址指向堆内存中的对象,是按引用访问的。

在堆内存中为这个值分配空间,由于这种值的大小不固定,因此不能把它们保存到栈内存中。但内存地址大小的固定的,因此可以将内存地址保存在栈内存中。 这样,当查询引用类型的变量时, 先从栈中读取内存地址, 然后再通过地址找到堆中的值。

综上:JS中对象分为基本类型和复合(引用)类型,基本类型存放在栈内存,复合(引用)类型存放在堆内存。堆内存用于存放由new创建的对象,栈内存存放一些基本类型的变量和对象的引用变量。

数组拷贝

对于简单的变量,内存小,我们直接用=赋值(浅拷贝)不会发生引用。所以值也就是拷贝的值,存放在新的变量对应的栈内存中就好。

然而对于数组和对象这种内存占用比较大的来说,直接用=复制的东西等于要复制的,那么就会发生引用,因为这种拷贝,只是将拷贝出来的东西的指向指向了要拷贝的那个东西,简单的说,就是两个都同时指向了一个空间,如果改变其中一个,另一个也会发生变化。这就发生了引用。

数组浅拷贝

这里先说说数组,数组在ES5以前拷贝也存在引用的问题。例如:浅拷贝数组:

1
2
3
4
5
6
7
8
var arr1=[1,2,3];
var arr2=arr1;
arr1.push(4);
alert(arr1); //1234
alert(arr2); //1234
arr2.push(5);
alert(arr1); //12345
alert(arr2); //12345

数组深拷贝

对于数组需要深拷贝的问题,我们一般用for循环就可以解决:

1
2
3
4
5
6
7
8
9
var arr1 = [1,2,3]
var arr2 = copyArr(arr1)
function copyArr(arr) {
let res = []
for (let i = 0; i < arr.length; i++) {
res.push(arr[i])
}
return res
}

这样就实现了数组的复制(深拷贝)。当然日常我们还会通过数组的一些方法实现数组的赋值,例如:

1
2
3
var arr2 = arr1.slice(0);

var arr3 = arr1.concat();

以上是eS5前的方法,在ES6我们数组拷贝有新的两种方法,不会发生引用问题:

第一种:Array.from(要复制的数组):

1
2
3
4
5
6
7
8
var arr1=[1,2,3];
var arr2=Array.from(arr1);
arr1.push(4);
alert(arr1); //1234
alert(arr2); //123
arr2.push(5);
alert(arr1); //1234
alert(arr2); //1235

第二种:…拓展运算符

1
2
3
4
5
6
7
8
var arr1=[1,2,3];
var arr2=[...arr1];
arr1.push(4);
alert(arr1); //1234
alert(arr2); //123
arr2.push(5);
alert(arr1); //1234
alert(arr2); //1235

…拓展运算符这个方法也可以用在函数的行参上面:

1
2
3
4
5
6
7
8
function show(...arr1){  //直接来复制arguments这个伪数组,让它变成真正的数组,从而拥有数组的方法。
alert(arr1); //1234
arr1.push(5);
alert(arr1); //12345
}
show(1,2,3,4)

//可以代替es5之前的apply/call 方法,例如:show.apply(this,arr1);

对象拷贝

对象浅拷贝

利用forin遍历拷贝对象,为浅拷贝对象:

1
2
3
4
5
6
7
8
9
10
11
12
var json1 = {"a":"cchroot","arr1":[1,2,3]}
function copy(obj1) {
    var obj2 = {};
    for (var i in obj1) {
      obj2[i] = obj1[i];
    }
    return obj2;
}
var json2 = copy(json1);
json1.arr1.push(4);
alert(json1.arr1); //1234
alert(json2.arr1) //1234

此外,还可以利用ES6中 Object.assign() 方法浅拷贝对象,bject.assign()拷贝的是属性值。假如源对象的属性值是一个指向对象的引用,它也只拷贝那个引用值。

1
2
3
var obj = { a: 1 };
var copy = Object.assign({}, obj);
console.log(copy); // { a: 1 }

对象深拷贝

拷贝和浅拷贝最根本的区别在于是否是真正获取了一个对象的复制实体,而不是引用

  1. 深复制在计算机中开辟了一块内存地址用于存放复制的对象
  2. 而浅复制仅仅是指向被复制的内存地址,如果原地址中对象被改变了,那么浅复制出来的对象也会相应改变。

目前实现深拷贝的方法不多,主要是两种:

  1. 利用 JSON 对象中的 parse 和 stringify
  2. 利用递归来实现每一层都重新创建对象并赋值

一、序列化反序列化法(JSON.stringify/parse的方法)

先看看这两个方法吧:

  • JSON.stringify 是将一个 JavaScript 值转成一个 JSON 字符串。
  • JSON.parse 是将一个 JSON 字符串转成一个 JavaScript 值或对象。

简单的说,就是 JavaScript 值和 JSON 字符串的相互转换。

我们可以封装一个方法:

1
2
3
4
// 序列化反序列化法
function deepClone(obj) {
return JSON.parse(JSON.stringify(obj))
}

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var obj = {
a:'a',
b:'b',
c:[1,2,3],
d:{dd:'dd',ee:'ee'}
};

function deepClone(obj) {
return JSON.parse(JSON.stringify(obj))
}

var obj2 = deepClone(obj);

console.log(obj === obj2); // false

上面对于简单的对象拷贝(数组)是没问题的,但是对于下面:

1
2
3
4
5
6
7
8
9
10
const originObj = {
name:'test',
unf: undefined,
nul: null,
saySomething:function(){
console.log('Hello World');
}
}
const resuleObj = deepClone(originObj);
console.log(resuleObj);//Object{name:'test',null:null}

发现在 resuleObj 中,unf属性和saySomething方法丢失了。。。那是为什么呢?

那是因为:undefined、function、symbol 会在转换过程中被忽略。。。

也就是说如果对象中含有一个函数时(很常见),就不能用这个方法进行深拷贝,这种方法比较适合平常开发中使用,因为通常不需要考虑对象和数组之外的类型

二、迭代递归法

我们要求复制一个复杂的对象,那么我们就可以利用递归的思想来做,及省性能,又不会发生引用。

递归的思想就很简单了,就是对每一层的数据都实现一次 创建对象->对象赋值 的操作,例如拷贝一个json对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var json1={"name":"cchroot","age":18,"arr1":[1,2,3,4,5],"string":'afasfsafa',"arr2":[1,2,3,4,5],"arr3":[{"name1":"cchroot"},{"job":"前端"}]};
var json2={};
function copy(obj1,obj2){
var obj2=obj2||{}; //最初的时候给它一个初始值=它自己或者是一个json
for(var name in obj1){
if(typeof obj1[name] === "object"){ //先判断一下obj[name]是不是一个对象
obj2[name]= (obj1[name].constructor===Array)?[]:{}; //我们让要复制的对象的name项=数组或者是json
copy(obj1[name],obj2[name]); //然后来无限调用函数自己 递归思想
}else{
obj2[name]=obj1[name]; //如果不是对象,直接等于即可,不会发生引用。
}
}
return obj2; //然后在把复制好的对象给return出去
}
json2=copy(json1,json2)
json1.arr1.push(6);
alert(json1.arr1); //123456
alert(json2.arr1); //12345

上面是封装的是一个拷贝对象的方法,下面我们可以封装一个拷贝对象和数组都可以的通用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function deepClone(source){
const targetObj = source.constructor === Array ? [] : {}; // 判断复制的目标是数组还是对象
for(let keys in source){ // 遍历目标
if(source.hasOwnProperty(keys)){
if(source[keys] && typeof source[keys] === 'object'){ // 如果值是对象,就递归一下
targetObj[keys] = source[keys].constructor === Array ? [] : {};
targetObj[keys] = deepClone(source[keys]);
}else{ // 如果不是,就直接赋值
targetObj[keys] = source[keys];
}
}
}
return targetObj;
}

我们可以再使用测试一下:

首先,对于普通对象,数组

1
2
3
4
5
6
7
8
9
10
const originObj = {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};
const cloneObj = deepClone(originObj);
console.log(cloneObj === originObj); // false

cloneObj.a = 'aa';
cloneObj.c = [1,1,1];
cloneObj.d.dd = 'doubled';

console.log(cloneObj); // {a:'aa',b:'b',c:[1,1,1],d:{dd:'doubled'}};
console.log(originObj); // {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};

以上是没有问题的,那再试试带有函数的:

1
2
3
4
5
6
7
8
9
10
11
const originObj = {
name:'test',
unf: undefined,
nul: null,
saySomething:function(){
console.log('Hello World');
}
}
console.log(originObj); // {name: "test", unf: undefined, nul: null, saySomething: ƒ}
const cloneObj = deepClone(originObj);
console.log(cloneObj); // {name: "test", unf: undefined, nul: null, saySomething: ƒ}

也是没有问题的。

总结

  1. 赋值运算符 = 实现的是浅拷贝,只拷贝对象的引用值,基本基本类型(值类型)的数据我们用=浅拷贝就行;
  2. 对于数组,ES5之前可以利用遍历和slice()concat()方法深拷贝,ES6开始,可以用Array.from()和…扩展运算符实现深拷贝
  3. 基于序列化反序列化法(JSON.stringify/parse的方法)实现的是深拷贝,但是对目标对象有要求(非 undefined,function等);
  4. 若想真正意义上的深拷贝,请递归。

参考文章:

js中对象的复制,浅复制(浅拷贝)和深复制(深拷贝)
对象的深拷贝和浅拷贝



完~