JavaScript中的深拷贝如何实现


今天小编给大家分享一下JavaScript中的深拷贝如何实现的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。这里先直接给出最终的代码版本,方便想快速了解的人查看,当然,你想一步步了解可以继续查看文章余下的内容:

functiondeepClone(target){
constmap=newWeakMap()

functionisObject(target){
return(typeoftarget==='object'&&target)||typeoftarget==='function'
}

functionclone(data){
if(!isObject(data)){
returndata
}
if([Date,RegExp].includes(data.constructor)){
returnnewdata.constructor(data)
}
if(typeofdata==='function'){
returnnewFunction('return'+data.toString())()
}
constexist=map.get(data)
if(exist){
returnexist
}
if(datainstanceofMap){
constresult=newMap()
map.set(data,result)
data.forEach((val,key)=>{
if(isObject(val)){
result.set(key,clone(val))
}else{
result.set(key,val)
}
})
returnresult
}
if(datainstanceofSet){
constresult=newSet()
map.set(data,result)
data.forEach(val=>{
if(isObject(val)){
result.add(clone(val))
}else{
result.add(val)
}
})
returnresult
}
constkeys=Reflect.ownKeys(data)
constallDesc=Object.getOwnPropertyDescriptors(data)
constresult=Object.create(Object.getPrototypeOf(data),allDesc)
map.set(data,result)
keys.forEach(key=>{
constval=data[key]
if(isObject(val)){
result[key]=clone(val)
}else{
result[key]=val
}
})
returnresult
}

returnclone(target)
}

先看看JS数据类型图(除了Object,其他都是基础类型):

在JavaScript中,基础类型值的复制是直接拷贝一份新的一模一样的数据,这两份数据相互独立,互不影响。而引用类型值(Object类型)的复制是传递对象的引用(也就是对象所在的内存地址,即指向对象的指针),相当于多个变量指向同一个对象,那么只要其中的一个变量对这个对象进行修改,其他的变量所指向的对象也会跟着修改(因为它们指向的是同一个对象)。如下图:
深浅拷贝主要针对的是Object类型,基础类型的值本身即是复制一模一样的一份,不区分深浅拷贝。这里我们先给出测试的拷贝对象,大家可以拿这个obj对象来测试一下自己写的深拷贝函数是否完善:

//测试的obj对象
constobj={
//===========1.基础数据类型===========
num:0,//number
str:'',//string
bool:true,//boolean
unf:undefined,//undefined
nul:null,//null
sym:Symbol('sym'),//symbol
bign:BigInt(1n),//bigint

//===========2.Object类型===========
//普通对象
obj:{
name:'我是一个对象',
id:1
},
//数组
arr:[0,1,2],
//函数
func:function(){
console.log('我是一个函数')
},
//日期
date:newDate(0),
//正则
reg:newRegExp('/我是一个正则/ig'),
//Map
map:newMap().set('mapKey',1),
//Set
set:newSet().add('set'),
//===========3.其他===========
[Symbol('1')]:1//Symbol作为key
};

//4.添加不可枚举属性
Object.defineProperty(obj,'innumerable',{
enumerable:false,
value:'不可枚举属性'
});

//5.设置原型对象
Object.setPrototypeOf(obj,{
proto:'proto'
})

//6.设置loop成循环引用的属性
obj.loop=obj

obj对象在Chrome浏览器中的结果:浅拷贝: 创建一个新的对象,来接受你要重新复制或引用的对象值。如果对象属性是基本的数据类型,复制的就是基本类型的值给新对象;但如果属性是引用数据类型,复制的就是内存中的地址,如果其中一个对象改变了这个内存中的地址所指向的对象,肯定会影响到另一个对象。首先我们看看一些浅拷贝的方法(详细了解可点击对应方法的超链接):这里只列举了常用的几种方式,除此之外当然还有其他更多的方式。注意,我们直接使用=赋值不是浅拷贝,因为它是直接指向同一个对象了,并没有返回一个新对象。手动实现一个浅拷贝:

functionshallowClone(target){
if(typeoftarget==='object'&&target!==null){
constcloneTarget=Array.isArray(target)?[]:{};
for(letpropintarget){
if(target.hasOwnProperty(prop)){
cloneTarget[prop]=target[prop];
}
}
returncloneTarget;
}else{
returntarget;
}
}


//测试
constshallowCloneObj=shallowClone(obj)

shallowCloneObj===obj//false,返回的是一个新对象
shallowCloneObj.arr===obj.arr//true,对于对象类型只拷贝了引用

从上面这段代码可以看出,利用类型判断(查看typeof),针对引用类型的对象进行 for 循环遍历对象属性赋值给目标对象的属性(for...in语句以任意顺序遍历一个对象的除Symbol以外的可枚举属性,包含原型上的属性。查看for…in),基本就可以手工实现一个浅拷贝的代码了。深拷贝:创建一个新的对象,将一个对象从内存中完整地拷贝出来一份给该新对象,并从堆内存中开辟一个全新的空间存放新对象,且新对象的修改并不会改变原对象,二者实现真正的分离。看看现存的一些深拷贝的方法:JSON.stringfy() 其实就是将一个 JavaScript 对象或值转换为 JSON 字符串,最后再用 JSON.parse() 的方法将JSON 字符串生成一个新的对象。(点这了解:JSON.stringfy()、JSON.parse())使用如下:

functiondeepClone(target){
if(typeoftarget==='object'&&target!==null){
returnJSON.parse(JSON.stringify(target));
}else{
returntarget;
}
}

//开头的测试obj存在BigInt类型、循环引用,JSON.stringfy()执行会报错,所以除去这两个条件进行测试
constclonedObj=deepClone(obj)

//测试
clonedObj===obj//false,返回的是一个新对象
clonedObj.arr===obj.arr//false,说明拷贝的不是引用

浏览器执行结果:
从以上结果我们可知JSON.stringfy() 存在以下一些问题:执行会报错:存在BigInt类型、循环引用。拷贝Date引用类型会变成字符串。键值会消失:对象的值中为FunctionUndefinedSymbol 这几种类型,。键值变成空对象:对象的值中为MapSetRegExp这几种类型。无法拷贝:不可枚举属性、对象的原型链。补充:其他更详细的内容请查看官方文档:JSON.stringify()由于以上种种限制条件,JSON.stringfy() 方式仅限于深拷贝一些普通的对象,对于更复杂的数据类型,我们需要另寻他路。手动递归实现深拷贝,我们只需要完成以下2点即可:对于基础类型,我们只需要简单地赋值即可(使用=)。对于引用类型,我们需要创建新的对象,并通过遍历键来赋值对应的值,这个过程中如果遇到 Object 类型还需要再次进行遍历。

functiondeepClone(target){
if(typeoftarget==='object'&&target){
letcloneObj={}
for(constkeyintarget){//遍历
constval=target[key]
if(typeofval==='object'&&val){
cloneObj[key]=deepClone(val)//是对象就再次调用该函数递归
}else{
cloneObj[key]=val//基本类型的话直接复制值
}
}
returncloneObj
}else{
returntarget;
}
}

//开头的测试obj存在循环引用,除去这个条件进行测试
constclonedObj=deepClone(obj)

//测试
clonedObj===obj//false,返回的是一个新对象
clonedObj.arr===obj.arr//false,说明拷贝的不是引用

浏览器执行结果:
该基础版本存在许多问题:不能处理循环引用。只考虑了Object对象,而Array对象、Date对象、RegExp对象、Map对象、Set对象都变成了Object对象,且值也不正确。丢失了属性名为Symbol类型的属性。丢失了不可枚举的属性。原型上的属性也被添加到拷贝的对象中了。如果存在循环引用的话,以上代码会导致无限递归,从而使得堆栈溢出。如下例子:

consta={}
constb={}
a.b=b
b.a=a
deepClone(a)

对象 a 的键 b 指向对象 b,对象 b 的键 a 指向对象 a,查看a对象,可以看到是无限循环的:

对对象a执行深拷贝,会出现死循环,从而耗尽内存,进而报错:堆栈溢出

如何避免这种情况呢?一种简单的方式就是把已添加的对象记录下来,这样下次碰到相同的对象引用时,直接指向记录中的对象即可。要实现这个记录功能,我们可以借助 ES6 推出的 WeakMap 对象,该对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。(WeakMap相关见这:WeakMap)针对以上基础版深拷贝存在的缺陷,我们进一步去完善,实现一个完美的深拷贝。对于基础版深拷贝存在的问题,我们一一改进:代码实现:

functiondeepClone(target){
//WeakMap作为记录对象Hash表(用于防止循环引用)
constmap=newWeakMap()

//判断是否为object类型的辅助函数,减少重复免费云主机域名代码
functionisObject(target){
return(typeoftarget==='object'&&target)||typeoftarget==='function'
}

functionclone(data){

//基础类型直接返回值
if(!isObject(data)){
returndata
}

//日期或者正则对象则直接构造一个新的对象返回
if([Date,RegExp].includes(data.constructor)){
returnnewdata.constructor(data)
}

//处理函数对象
if(typeofdata==='function'){
returnnewFunction('return'+data.toString())()
}

//如果该对象已存在,则直接返回该对象
constexist=map.get(data)
if(exist){
returnexist
}

//处理Map对象
if(datainstanceofMap){
constresult=newMap()
map.set(data,result)
data.forEach((val,key)=>{
//注意:map中的值为object的话也得深拷贝
if(isObject(val)){
result.set(key,clone(val))
}else{
result.set(key,val)
}
})
returnresult
}

//处理Set对象
if(datainstanceofSet){
constresult=newSet()
map.set(data,result)
data.forEach(val=>{
//注意:set中的值为object的话也得深拷贝
if(isObject(val)){
result.add(clone(val))
}else{
result.add(val)
}
})
returnresult
}

//收集键名(考虑了以Symbol作为key以及不可枚举的属性)
constkeys=Reflect.ownKeys(data)
//利用Object的getOwnPropertyDescriptors方法可以获得对象的所有属性以及对应的属性描述
constallDesc=Object.getOwnPropertyDescriptors(data)
//结合Object的create方法创建一个新对象,并继承传入原对象的原型链,这里得到的result是对data的浅拷贝
constresult=Object.create(Object.getPrototypeOf(data),allDesc)

//新对象加入到map中,进行记录
map.set(data,result)

//Object.create()是浅拷贝,所以要判断并递归执行深拷贝
keys.forEach(key=>{
constval=data[key]
if(isObject(val)){
//属性值为对象类型或函数对象的话也需要进行深拷贝
result[key]=clone(val)
}else{
result[key]=val
}
})
returnresult
}

returnclone(target)
}



//测试
constclonedObj=deepClone(obj)
clonedObj===obj//false,返回的是一个新对象
clonedObj.arr===obj.arr//false,说明拷贝的不是引用
clonedObj.func===obj.func//false,说明function也复制了一份
clonedObj.proto//proto,可以取到原型的属性

在遍历 Object 类型数据时,我们需要把 Symbol 类型的键名也考虑进来,所以不能通过 Object.keys 获取键名或 for...in 方式遍历,而是通过Reflect.ownKeys()获取所有自身的键名(getOwnPropertyNamesgetOwnPropertySymbols 函数将键名组合成数组也行:[...Object.getOwnPropertyNames(obj), ...Object.getOwnPropertySymbols(obj)]),然后再遍历递归,最终实现拷贝。浏览器执行结果:

可以发现我们的cloneObj对象和原来的obj对象一模一样,并且修改cloneObj对象的各个属性都不会对obj对象造成影响。其他的大家再多尝试体会哦!以上就是“JavaScript中的深拷贝如何实现”这篇文章的所有内容,感谢各位的阅读!相信大家阅读完这篇文章都有很大的收获,小编每天都会为大家更新不同的知识,如果还想学习更多的知识,请关注百云主机行业资讯频道。

相关推荐: 如何使用PHP查询IP地址归属地

这篇文章主要介绍了如何使用PHP查询IP地址归属地的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇如何使用PHP查询IP地址归属地文章都会有所收获,下面我们一起来看看吧。 PHP查询IP地址归属地的步骤:1、开通IP地址归属地接口服…

免责声明:本站发布的图片视频文字,以转载和分享为主,文章观点不代表本站立场,本站不承担相关法律责任;如果涉及侵权请联系邮箱:360163164@qq.com举报,并提供相关证据,经查实将立刻删除涉嫌侵权内容。

(0)
打赏 微信扫一扫 微信扫一扫
上一篇 02/16 16:23
下一篇 02/16 16:23

相关推荐