JS之深浅拷贝

浅拷贝导致的问题

js中我们有时候需要对数组或者对象这些复杂类型进行拷贝,当我们把他们赋予其他变量时就以为万事大吉了,可实际上当我们对改变这些变量时,原来的数组或者对象也会跟着改变,如下面的例子:

1
2
3
4
5
6
7
8
var a = [1,2,3]
var arr = a;
arr[1] = 1;
console.log(a[1]);//1
console.log(arr[1]);//1
console.log(a=arr);//true

是的,最后打印出来的都会是相同的结果,这也证明了我们的拷贝与原来的对象并不是完全分离的,这其实是浅拷贝导致的问题。

但是有时候我们想要获得跟原对象完全不相干的拷贝,那应该怎么办呢,这时候就需要深拷贝了

深浅拷贝的原理及区别

基本类型和引用类型

ECMAScript中类型变量分为两类:

  • 基本数据类型:number/string/boolean/undefined/null/symbol(ES6新增)
  • 引用数据类型:object/Array/Date/Function等

平时我们总听说堆栈,其实他们就是用来储存我们的变量和值的。

栈内存

对于基本数据类型而言,它们是保存在栈内存里面的,包含变量名对应的值
结构大致如下:

变量
a 55

堆内存

引用类型则是保存在堆内存中,栈内存中会保存有变量名以及引用类型在堆内存中的地址。当我们访问引用类型时,会先通过变量名在栈内存中找到对象在堆内存中的地址指针,然后再到相应的堆内存中查找数据。
现在栈内存的结构是这样的:

栈内存

变量
a 堆地址1

堆内存

栈内存中的堆地址1 —> 对内存中的obj1

obj1

栈内存中存放的必须时大小固定的数据,而引用类型大小不固定,只能存放在堆中。基本数据类型是按值访问,而引用类型是按地址访问。

基本类型的复制

对于基本类型来说,当拷贝时,会将值也一同复制给新变量,当我们修改它时,是下面这样的:

1
2
3
4
var a = 1;
var b = a;
b = 2;
console.log(a);//1

从上面也可以看出,修改b后,并不会影响到a的值,因为它们是相互独立的。

栈内存中对于相同的基本数据类型值只会保存一份,并不会存放两个1,如果两个变量都指向1,那么他们就是相等的,指向不同值,则是不等(变量相互独立)的。函数参数的按值传递跟上面的例子原理一样,所以函数内就算重新修改了新变量也不会改变原来的变量。

引用类型的复制

1
2
3
4
5
var a = [1,2,3];
var b = a;
b.push(4);
console.log(a);//[1,2,3,4]
console.log(b);//[1,2,3,4]

虽然我们的本意是只想对拷贝后的b进行修改,但是由于我们拷贝的只是a的引用类型地址,所以其实他们指向的是同一个对象,这时候无论对谁进行修改都会导致两者都发生变化。

###深浅拷贝
聪明的你一定会想到下面这种方法:

1
2
3
4
5
6
7
var a = [1,2,3];
var b = [];
for(var i = 0;i<a.length;i++){
b[i] = a[i];
}
b.pop();
console.log(a);//[1,2,3]

很好,你会发现现在就算我们修改了新对象,原对象也并不会发生改动(浅复制),但如果你以为这样就成功了,那还有我们深拷贝什么事呢!

仔细看,你会发现我们的引用类型中的数据都是基本数据类型,那么换成引用类型又会发生什么呢?

1
2
3
4
5
6
7
8
9
var a = [[1,2,3],2,3];
var b = [];
for(var i = 0;i<a.length;i++){
b[i] = a[i];
}
b[0].pop();
console.log(a);//[[1,2],2,3]
b.shift();
console.log(a);//[[1,2],2,3]

现在,你会无奈的发现,新建了对象后并没有什么卵用,其实还是引用类型在作怪,谁让它是通过栈内存中地址来指向的呢。

很明显,外层的修改并不会有什么问题,但是一旦涉及到引用类型,那么又会回到前面的老问题上面!

既然一层的复制不行,那么我们就用递归的方式对下面嵌套的所有引用类型一一进行浅复制,直到最后都转换为基本数据类型的拷贝。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//深拷贝函数
function deepClone(obj){
var newObj = {};
if((typeof obj) !== 'object'){
return obj
}else{
for(var attr in obj){
newObj[attr] = arguments.callee(obj[attr])
}
}
return newObj;
}
var a = {
name:'liby',
skills:{
language:'CET6',
computer:['js','python','node'],
others:['skating']
}
}
var b = deepClone(a);
b.skills.others.push('Balisong');
console.log(a);//nothing changed

现在,深拷贝完成,a和b已经不会再产生交集了,随便我们怎么折腾,哈哈!

数组和对象原生方法实现深浅拷贝

上面我们用自己写的方法递归实现了深复制,但是其实用对象提供的一些原生方法完全能够做到深浅拷贝,一起来记录一下吧!

数组

数组有4种方法可以实现浅拷贝:concat()和slice(),Array.from(),扩展运算符

slice()

slice方法可用来在原数组上面分割形成新数组,第一个参数为0时,即切割全部,返回一个新数组

1
2
3
4
var arr = [1,2,3,4,5]
var newArr = arr.slice(0)
newArr[0] = 6
console.log(arr);//[1,2,3,4,5]

concat()

concat方法用来合并两个数组,不传入参数则深拷贝此数组

1
2
3
4
var arr = [1,2,3,4,5]
var newArr = arr.concat();
newArr[0] = 6;
console.log(arr);//[1,2,3,4,5]

Array.from()

这时ES6中的方法,用来将类数组转换为真正的数组,当然也可以用来深拷贝

1
2
3
4
var arr = [1,2,3,4,5]
var newArr = Array.from(arr);
newArr[0] = 6
console.log(arr);//[1,2,3,4,5]

…(扩展运算符)

同样也是ES6的东西,能够将一个数组轻易的解构并按照同样的模式赋值给新对象,其实内部是用的迭代器遍历复制,跟我们之前遍历复制很像,不过代码量少得多,哈哈,这也是浅拷贝!

1
2
3
4
var arr = [1,2,3,4,5]
var newArr = [...arr];
newArr[0] = 6
console.log(arr);//[1,2,3,4,5]

对象的拷贝

object.assign()(浅拷贝)

object.assign()用来将任意uoge对象自身的可枚举属性拷贝给目标对象,不过拷贝的只是对象的引用,而不是对象本身,即浅拷贝。

JSON.parse()

将元对象转成JSON字符串再转回来,最简单粗暴的一种深拷贝方法!!!

1
2
var obj = {'name':'liby','course':{'english':80}};
var newObj = JSON.parse(JSON.stringify(obj));

jQuery.extend()

jQuery.extend这个扩展对象的方法也可以用来进行深拷贝,需要传入true参数。

深浅拷贝的原理及方法已介绍完,如有遗漏,后续会补充上来!