Js中数据类型分为基本数据类型(number,string,boolean,null,undefined,symbol,BigInt)以及引用数据类型(Object、Array、Function等)
基本数据类型
- 基本数据类型的名值都存储在栈内存中
- 如果let a = 1,当b = a复制时,栈内存会新开辟一个内存
- 此时修改a = 2,不会对b造成影响,因为他们存储值的位置是互相独立互不干扰的
- 所以基本数据类型不存在浅拷贝
let a = 1;
let b = a;
a = 2;
console.log(a); //2
console.log(b); //1
console.log(a === b); //false
以上过程可以表示为:
引用数据类型
- 名称与值不会全部存在栈内存中,仅有名称会存在于栈内存中,值会存在于堆内存中
- 栈内存会提供一个引用的地址指向堆内存的值
- 如果a为一个数组,元素有[0, 1, 2];当b = a复制时,会复制的是a的引用地址,而并非堆里面的值,a与b虽然是两个数组,但都会指向同一个值
- 而当我们a[0]=3时进行数组修改时,由于a与b指向的是同一个地址,所以此时输出b,会发现b做出了同等改变,这就是浅拷贝
- 我们需要使用深拷贝的方法来避免上述问题的出现
let a = [0, 1, 2];
let b = a;
a[0] = 3;
console.log(a); //[ 3, 1, 2 ]
console.log(b); //[ 3, 1, 2 ]
console.log(a === b); //true
当我们对数组a进行修改时,修改的是堆内存存储的值,引用地址不变。
由于在堆内存中没有专门开辟一个新的内存为b存放值,所以输出b时,我们会发现b被同等地改变了
所以我们在处理引用数据类型的时候,切忌使用b = a 这种方法进行浅拷贝,不然可能会产生很多莫名其妙的bug
深拷贝的实现方法
想要实现深拷贝,就需要把引用数据类型的值完完全全地复制一份并赋值给另外的一个元素,而不是引用
既然是深拷贝,那拷贝的必须要足够完全
伪深拷贝之slice
对于数组,网上流传着一种深拷贝方法是使用没有任何参数的slice函数进行复制,譬如
const arr2 = arr.slice()
slice函数通过索引位置获取新的数组,该方法不会修改原数组,只是返回一个新的子数组。
let arr = [0, 1, 2];
const arr2 = arr.slice();
arr[0] = 3;
console.log(arr); //[ 3, 1, 2 ]
console.log(arr2); //[ 0, 1, 2 ]
console.log(arr === arr2); //false
乍一看,我们使用slice函数拷贝出来的arr2似乎完全脱离了arr的控制,不受arr内容修改的任何影响,但是,且看:
let arr = [ [0, 1, 2], 1, 2 ];
const arr2 = arr.slice();
arr[0][0] = 3;
console.log(arr); //[ [ 3, 1, 2 ], 1, 2 ]
console.log(arr2); //[ [ 3, 1, 2 ], 1, 2 ]
console.log(arr === arr2); //false
- 可以看到的是,slice函数仅仅对数组arr的外层进行了深拷贝,而数组内部嵌套的其他引用数据类型,依然通过浅拷贝的方式被赋值给了arr2
- arr2仍然脱离不了a的控制,说明slice根本不是真正的深拷贝。
- 至于为什么arr === arr2输出的结果是false,这是因为==与===比较的是他们的引用(内存地址),也就是栈内存里储存的堆地址,他们堆地址的指向不同,就算指向的内容完全相同,结果也为false
- 同样的,作为“常量”被定义为const的arr2,它的值依然会发生改变。这说明const定义的不是常量,而是特殊的变量。const保证的并不是变量的值不能改动,而是变量指向的那个内存地址不能改动
- 不过如果想要拷贝的数组不会出现二级属性的情况,slice函数依然可以作为简便的拷贝方法使用
同理,concat方法与slice也存在这样的情况,他们都不是真正的深拷贝。
JSON.parse(JSON.stringify(obj))
至于如何真正实现浅拷贝,由于JSON会自己去构建新的内存来存放新对象,所以一个十分简单好用的方法便是JSON.parse(JSON.stringify(obj))
let arr = [ [0, 1, 2], 1, 2 ];
let arr2 = JSON.parse(JSON.stringify(arr));
arr[0][0] = 3;
console.log(arr); //[ [ 3, 1, 2 ], 1, 2 ]
console.log(arr2); //[ [ 0, 1, 2 ], 1, 2 ]
可以看到的是,arr2彻底脱离了arr的束缚,不论是一级还是二级属性,均不会随着arr的改变而改变
需要注意的是,由于JSON格式字符串不支持Function,所以不可以对Function进行拷贝
实现深拷贝的方法还有许多,譬如递归、Object.assgin、MessageChannel以及lodash.cloneDeep等等,但笔者才疏学浅,这些方法还需要深入探究之后,才能有心得记录在此
更多内容,参见:【JS】深拷贝与浅拷贝的区别,实现深拷贝的几种方法