js关于深拷贝与浅拷贝问题

前言

谈到js的拷贝,其实分为三种:赋值、浅拷贝和深拷贝。赋值是经常用到的操作,但实际上基本数据类型与引用数据类型的存储方式是不同,第一种是将值存在栈内存中的,而另一个只是在栈内存中存了指向堆内存数据的指针。具体不在这里赘述。今天要讨论的深拷贝与浅拷贝。

区别

在说区别之前需要做一个铺垫,我们需要了解几个概念。
1.基本数据类型是在内存中占据固定大小的,保存在栈内存(栈区、stack)中。
2.引用数据类型是保存在堆内存(堆区、heap)中,在栈内存(栈区、stack)中存储只是变量标识符与指向堆中该实体起始地址的指针。
小知识
闭包中的变量并不保存在栈内存中,而是保存在堆内存中。例子如下:

1
2
3
4
5
6
7
8
9
function A() {
let a = 'right'
function B() {
console.log(a)
}
return B
}
var makeFunc = A()
makeFunc()//right

这里闭包中的变量如果保存在了栈内存中,随着外层中的函数从调用栈中销毁,变量肯定额会被销毁,但是如果是保存在堆内存中,内存函数仍然能访问外层已经销毁函数中的变量。
弄清楚以上这些之后,我们简单来说一下两种拷贝方式的区别。

深拷贝:将B对象拷贝到A对象中,包括B里面的子对象。
浅拷贝:将B对象拷贝到A对象中,但不包括B里面的子对象。

详细来讲,前言中提到的赋值操作,在使用基本类型数据时,会在栈区产生两个独立相互不影响的变量。
但是引用类型,只是改变了引用的指针,但是仍然指向的是堆区的同一个对象,所以相互之间就会产生影响。
而浅拷贝则是重新在堆区创建内存,并且引用类型在第一层时互不影响,但在改变第二层属性时会互相影响。
深拷贝则完全不会受到影响,是完全复制了对象及其属性,是对对象的子对象进行递归拷贝。

例子

1.Object.assign就是一种浅拷贝,只是拷贝了对象的属性的引用。

1
2
3
4
var obj = { a:{a: "hello", b: 21} }
var initalObj = Object.assign({}, obj)
initalObj.a.a = "How are you?"
console.log(obj.a.a)// How are you

对于深层的属性,只是拷贝了栈中的指针。

1
2
3
4
5
var objA = { a: 10, b: 20, c: 30 }
var objB = Object.assign({}, objA)
objB.b = 100
console.log(objA)//{a: 10, b: 20, c: 30}
console.log(objB)//{a: 10, b: 100, c: 30}

对于第一层则是可以。

2.手动复制

1
2
3
4
5
6
7
var obj1 = { a: 10, b: 20, c: 30 };
var obj2 = { a: obj1.a, b: obj1.b, c: obj1.c };
obj2.b = 100;
console.log(obj1);
// { a: 10, b: 20, c: 30 } <-- 沒被改到
console.log(obj2);
// { a: 10, b: 100, c: 30 }

这种方式仍然只是可以在一层时可以不相互影响,针对多层时,这种方式依然可以实现,但就显得格外麻烦

1
2
3
4
5
var obj1 = {a:{a:"right",b:"false"}}
var obj2 = {a:{a:obj.a.a,b:obj.a.b}}
obj2.a.a = "what"
console.log(obj2.a.a)//what
console.log(obj1.a.a)//right

这里如果是var obj3 = {a:obj1.a}时,则会互相影响,但同样是影响第二层,第一层不会相互影响。

3.JSON.stringfy把对象转换成字符串,再用JSON.parse把字符串转成新的对象。

1
2
3
4
5
6
7
8
9
10
11
var obj1 = { body: { a: 10 } };
var obj2 = JSON.parse(JSON.stringify(obj1));
obj2.body.a = 20;
console.log(obj1);
// { body: { a: 10 } } <-- 沒被改到
console.log(obj2);
// { body: { a: 20 } }
console.log(obj1 === obj2);
// false
console.log(obj1.body === obj2.body);
// false

这是一种深拷贝。但是这种方法也有不少坏处,譬如它会抛弃对象的constructor。也就是深拷贝之后,不管这个对象原来的构造函数是什么,在深拷贝之后都会变成Object。
这种方法能正确处理的对象只有 Number, String, Boolean, Array, 扁平对象,即那些能够被 json 直接表示的数据结构。RegExp对象是无法通过这种方式深拷贝。
也就是说,只有可以转成JSON格式的对象才可以这样用,像function没办法转成JSON。

1
2
3
4
5
6
var obj1 = { fun: function(){ console.log(123) } };
var obj2 = JSON.parse(JSON.stringify(obj1));
console.log(typeof obj1.fun);
// 'function'
console.log(typeof obj2.fun);
// 'undefined' <-- 没复制

4.递归拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function deepClone(initalObj, finalObj) {    
var obj = finalObj || {};
for (var i in initalObj) {
var prop = initalObj[i]; // 避免相互引用对象导致死循环,如initalObj.a = initalObj的情况
if(prop === obj) {
continue;
}
if (typeof prop === 'object') {
obj[i] = (prop.constructor === Array) ? [] : {};
arguments.callee(prop, obj[i]);
} else {
obj[i] = prop;
}
}
return obj;
}
var str = {};
var obj = { a: {a: "hello", b: 21} };
deepClone(obj, str);
console.log(str.a);

5.使用Object.create()方法

这是一种深拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function deepClone(initalObj, finalObj) {    
var obj = finalObj || {};
for (var i in initalObj) {
var prop = initalObj[i]; // 避免相互引用对象导致死循环,如initalObj.a = initalObj的情况
if(prop === obj) {
continue;
}
if (typeof prop === 'object') {
obj[i] = (prop.constructor === Array) ? [] : Object.create(prop);
} else {
obj[i] = prop;
}
}
return obj;
}

参考链接

搞不懂JS中赋值·浅拷贝·深拷贝的请看这里
js 深拷贝 vs 浅拷贝
js浅拷贝与深拷贝方法
闭包