一直以来,都在用深拷贝。但从没自己实现过,想着是挺简单的,其实动手做还是有点坑的。
概念:
普通的赋值,如果是引用类型(object)的话,如果把a赋值给b,那么我改变b,a也会变化,这个是浅拷贝。深拷贝则是与之相反,赋值与被赋值的怎样改变,也不会影响彼此。 这也是我们大多数时候想要的。
原理:
之所以会出现深浅拷贝的情况,是因为普通赋值如果是引用类型,那么js并不会在堆栈中新开辟空间来存储,而是仅仅将其指向堆栈中数据,这样本质上其实最终无论操作被赋值或者赋值的变量,其实都相当于操作堆栈中的同一个数据;
对应的,js遇到基础类型(String、Boolean、Undefined、Null、Number、Symbol),或其他非引用类型则会在堆栈中新创建空间来储存,所以基础类型不存在深浅拷贝的说法,也可以将此认为是实现深拷贝的基本(个人理解)
实现方法:
先把可实现方法列出来:assign,递归全赋值,JSON.parse(JSON.stringify()),function return
首先说一说不该叫深拷贝,但确实又不必担心深浅拷贝问题的 —— function return
先看一串代码:
function getObj() {
return {
a:{
b:{
c:1
}
}
}
}
let obj = getObj()
let obj2 = getObj()
obj2.a.b.c=6
console.log(obj.a.b.c) // 打印1
结果上,obj与obj2互不影响,那么确实是深拷贝效果,但是此操作不大能算是拷贝,所以我们改变下代码,如下:
let o = {
a:{
b:{
c:1
}
}
}
function getObj() {
return o
}
let obj = getObj()
let obj2 = getObj()
obj2.a.b.c=6
console.log(obj.a.b.c) // 打印6
我们发现,并未达到我们的预期,也就是说,此方法想达到深拷贝的效果只能是预先写好的,所以我们想拿到一个变量后通过此方法将其深拷贝是不可行的,这也是我为什么说他不算是拷贝。当然,项目中写一写初始信息,配置信息类的用起来是非常好用的。
关于原理,其实看一串代码要比打字说明理解快:
function getObj(){
return {
a:1
}
}
let o1 = getObj()
let o2 = getObj()
// 等价于
let o1 = {
a:1
}
let o2 = {
a:1
}
通过return一个变量不可以深拷贝,也是同理,因为相当于把此变量反复赋值。
号称乞丐版,但也最简单最实用,并且坑最多的深拷贝 —— JSON.parse(JSON.stringify())
上面提到过,js遇到基础类型会在堆栈中新建空间来储存,也就是说所有基础类型进行赋值都是深拷贝,这也正是JSON.parse(JSON.stringify())能实现深拷贝的原理,在JSON.stringify()的时候,就已经变为了String并且和被赋值变量脱离关系,再将String转为JSON后则完全是一个新的数据了。因为其中数据变为了String,所以则不会有引用关系(首尾呼应,基础类型全民轻易深拷贝),所以实现了深拷贝。
这个方法的坑:可以这么说,这个方法多简单,他的坑就有多多! 实践中除非需要深拷贝的数据为最基础最基础的json数据,不然别用。简单罗列下会遇到的问题:构造函数丢失,function丢失,Undefined丢失、正则表达式变为{}、Data变成String、Set类型变Array,Map类型变Array。由于太多,可能还有一些没想起来的。。。
虽然,坑这么多。但我们数据大多时候并没这么复杂,所以说——真香!
可以实现完美的深拷贝—— assign,递归全赋值
这个没什么特别说的,遇到深度为一的对象,assign直接可以实现深拷贝,多维的就是循环递归,将所有的都assign掉,这个与全赋值的写法差不多,下面直接贴下全赋值的深拷贝方法:
function deepClone(obj) {
let clone = {}
if(obj instanceof RegExp) return new RegExp(obj);
if(obj instanceof Date) return new Date(obj);
if(obj === null) return null;
if(typeof obj !== 'object') return obj;
if (Array.isArray(obj)) {
clone = []; // 将result赋值为一个数组,并且执行遍历
for (let i in obj) {
// 递归克隆数组中的每一项
clone.push(deepClone(obj[i]))
}
// 判断如果当前的值是对象,那么逐个赋值,并对各个值深拷贝
} else {
for(let key in obj) {
// 此操作虽然是简单的赋值,但却涵盖了一切可能,无论被拷贝对象是什么类型都不会丢失构造函数等
clone[key]=obj[key]
// 继续深入拷贝
deepClone(clone[key])
}
}
return clone;
}
此方法还缺少了对Set、Map类型的判断,如需要可进行判断并处理;可以说是很多类型需要单独判断是这个方法唯一的坑了,当有问题时候检查下是不是对应类型没特殊处理准没错。
总结: 其实我们有发现,无论是哪种深拷贝方法,数据都是止于非引用类型,然后实现了深拷贝(递归赋值最终也要进行到非引用类型才不继续递归),所以说非引用类型的特性才是我们能实现深拷贝的根本(首尾呼应,个人理解,毕竟非引用类型没指针问题)。