当前位置:网站首页>实现深拷贝的几种方式
实现深拷贝的几种方式
2022-07-19 22:11:00 【qq_25741071】
目前使用过四种
1、递归递归去复制所有层级属性
function deepClone(obj){
let objClone = Array.isArray(obj)?[]:{
};
if(obj && typeof obj==="object"){
for(key in obj){
if(obj.hasOwnProperty(key)){
//判断ojb子元素是否为对象,如果是,递归复制
if(obj[key]&&typeof obj[key] ==="object"){
objClone[key] = deepClone(obj[key]);
}else{
//如果不是,简单复制
objClone[key] = obj[key];
}
}
}
}
return objClone;
}
let a=[1,2,3,4],
b=deepClone(a);
a[0]=2;
console.log(a,b);
跟之前想象的一样,现在b脱离了a的控制,不再受a影响了。
这里再次强调,深拷贝,是拷贝对象各个层级的属性,可以看个例子。JQ里有一个extend方法也可以拷贝对象,我们来看看
let a=[1,2,3,4],
b=a.slice();
a[0]=2;
console.log(a,b);
那是不是说slice方法也是深拷贝了,毕竟b也没受a的影响,上面说了,深拷贝是会拷贝所有层级的属性,还是这个例子,我们把a改改
let a=[0,1,[2,3],4],
b=a.slice();
a[0]=1;
a[2][0]=1;
console.log(a,b);
拷贝的不彻底啊,b对象的一级属性确实不受影响了,但是二级属性还是没能拷贝成功,仍然脱离不了a的控制,说明slice根本不是真正的深拷贝。
这里引用知乎问答里面的一张图
第一层的属性确实深拷贝,拥有了独立的内存,但更深的属性却仍然公用了地址,所以才会造成上面的问题。
同理,concat方法与slice也存在这样的情况,他们都不是真正的深拷贝,这里需要注意。
2.除了递归,我们还可以借用JSON对象的parse和stringify
function deepClone(obj){
let _obj = JSON.stringify(obj),
objClone = JSON.parse(_obj);
return objClone
}
let a=[0,1,[2,3],4],
b=deepClone(a);
a[0]=1;
a[2][0]=1;
console.log(a,b);
可以看到,这下b是完全不受a的影响了。
附带说下,JSON.stringify与JSON.parse除了实现深拷贝,还能结合localStorage实现对象数组存储。有兴趣可以阅读博主这篇文章。
localStorage存储数组,对象,localStorage,sessionStorage存储数组对象
3.除了上面两种方法之外,我们还可以借用JQ的extend方法。
$.extend( [deep ], target, object1 [, objectN ] )
deep表示是否深拷贝,为true为深拷贝,为false,则为浅拷贝
target Object类型 目标对象,其他对象的成员属性将被附加到该对象上。
object1 objectN可选。 Object类型 第一个以及第N个被合并的对象。
let a=[0,1,[2,3],4],
b=$.extend(true,[],a);
a[0]=1;
a[2][0]=1;
console.log(a,b);
可以看到,效果与上面方法一样,只是需要依赖JQ库。
说了这么多,了解深拷贝也不仅仅是为了应付面试题,在实际开发中也是非常有用的。例如后台返回了一堆数据,你需要对这堆数据做操作,但多人开发情况下,你是没办法明确这堆数据是否有其它功能也需要使用,直接修改可能会造成隐性问题,深拷贝能帮你更安全安心的去操作数据,根据实际情况来使用深拷贝,大概就是这个意思。
4.lodash的_.cloneDeep()
参考博客:https://www.cnblogs.com/echolun/p/7889848.html
以下是我参看的一位关于深拷贝的问题解决。
JSON.parse
先将一个对象转为json对象。然后再解析这个json对象。
let obj = {
a:{
b:22}};
let copy = JSON.parse(JSON.stringify(obj));
这种方法的优点就是代码写起来比较简单。但是缺点也是显而易见的。你先是创建一个临时的,可能很大的字符串,只是为了把它重新放回解析器。另一个缺点是这种方法不能处理循环对象。
如下面的循环对象用这种方法的时候会抛出异常
let a = {
};
let b = {
a};
a.b = b;
let copy = JSON.parse(JSON.stringify(a));
诸如 Map, Set, RegExp, Date, ArrayBuffer 和其他内置类型在进行序列化时会丢失。
let a = {
};
let b = new Set();
b.add(11);
a.test = b;
let copy = JSON.parse(JSON.stringify(a));
a 的值打印如下
copy的值打印如下
对比发现,Set已丢失。
Structured Clone 结构化克隆算法
MessageChannel
建立两个端,一个端发送消息,另一个端接收消息。
function structuralClone(obj) {
return new Promise(resolve =>{
const {
port1, port2} = new MessageChannel();
port2.onmessage = ev => resolve(ev.data);
port1.postMessage(obj);
})
}
const obj = /* ... */;
structuralClone(obj).then(res=>{
console.log(res);
})
这种方法的优点就是能解决循环引用的问题,还支持大量的内置数据类型。缺点就是这个方法是异步的。
History API
利用history.replaceState。这个api在做单页面应用的路由时可以做无刷新的改变url。这个对象使用结构化克隆,而且是同步的。但是我们需要注意,在单页面中不要把原有的路由逻辑搞乱了。所以我们在克隆完一个对象的时候,要恢复路由的原状。
function structuralClone(obj) {
const oldState = history.state;
history.replaceState(obj, document.title);
const copy = history.state;
history.replaceState(oldState, document.title);
return copy;
}
var obj = {
};
var b = {
obj};
obj.b = b
var copy = structuralClone(obj);
console.log(copy);
这个方法的优点是。能解决循环对象的问题,也支持许多内置类型的克隆。并且是同步的。但是缺点就是有的浏览器对调用频率有限制。比如Safari 30 秒内只允许调用 100 次
Notification API
这个api主要是用于桌面通知的。如果你使用Facebook的时候,你肯定会发现时常在浏览器的右下角有一个弹窗,对就是这家伙。我们也可以利用这个api实现js对象的深拷贝。
function structuralClone(obj) {
return new Notification('', {
data: obj, silent: true}).data;
}
var obj = {
};
var b = {
obj};
obj.b = b
var copy = structuralClone(obj);
console.log(copy)
同样是优点和缺点并存,优点就是可以解决循环对象问题,也支持许多内置类型的克隆,并且是同步的。缺点就是这个需要api的使用需要向用户请求权限,但是用在这里克隆数据的时候,不经用户授权也可以使用。在http协议的情况下会提示你再https的场景下使用。
lodash的_.cloneDeep()
支持循环对象,和大量的内置类型,对很多细节都处理的比较不错。推荐使用。
支持的类型有很多
我们这里再次关注一下lodash是如何解决循环应用这个问题的?
从相关的代码中。我们可以发现。lodash是用一个栈记录了。所有被拷贝的引用值。如果再次碰到同样的引用值的时候,不会再去拷贝一遍。而是利用之前已经拷贝好的值。
lodash深拷贝的详细的源码可以在这里查看。
https://github.com/lodash/lodash/blob/master/cloneDeep.js
实现一个简易点的深拷贝,以解决循环引用的问题为目标
我们仅仅实现一个简易点的深拷贝。能优雅的处理循环引用的即可。在实现深拷贝之前,我们首先温习回顾一下js中的遍历对象的属性的方法和各种方法的优缺点。
js中遍历一个对象的属性的方法
- Object.keys() 仅仅返回自身的可枚举属性,不包括继承来的,更不包括Symbol属性
- Object.getOwnPropertyNames() 返回自身的可枚举和不可枚举属性。但是不包括Symbol属性
- Object.getOwnPropertySymbols() 返回自身的Symol属性
- for…in 可以遍历对象的自身的和继承的可枚举属性,不包含Symbol属性
- Reflect.ownkeys() 返回对象自身的所有属性,不管是否可枚举,也不管是否是Symbol。注意不包括继承的属性
实现深拷贝,解决循环引用问题
/** * 判断是否是基本数据类型 * @param value */
function isPrimitive(value){
return (typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'symbol' ||
typeof value === 'boolean')
}
/** * 判断是否是一个js对象 * @param value */
function isObject(value){
return Object.prototype.toString.call(value) === "[object Object]"
}
/** * 深拷贝一个值 * @param value */
function cloneDeep(value){
// 记录被拷贝的值,避免循环引用的出现
let memo = {
};
function baseClone(value){
let res;
// 如果是基本数据类型,则直接返回
if(isPrimitive(value)){
return value;
// 如果是引用数据类型,我们浅拷贝一个新值来代替原来的值
}else if(Array.isArray(value)){
res = [...value];
}else if(isObject(value)){
res = {
...value};
}
// 检测我们浅拷贝的这个对象的属性值有没有是引用数据类型。如果是,则递归拷贝
Reflect.ownKeys(res).forEach(key=>{
if(typeof res[key] === "object" && res[key]!== null){
//此处我们用memo来记录已经被拷贝过的引用地址。以此来解决循环引用的问题
if(memo[res[key]]){
res[key] = memo[res[key]];
}else{
memo[res[key]] = res[key];
res[key] = baseClone(res[key])
}
}
})
return res;
}
return baseClone(value)
}
验证我们写的cloneDeep是否能解决循环应用的问题
var obj = {
};
var b = {
obj};
obj.b = b
var copy = cloneDeep(obj);
console.log(copy);
完美。大功告成
我们虽然的确解决了深拷贝的大部分问题。不过很多细节还没有去处理。在生产环境,我们还是要使用lodash的cloneDeep。cloneDeep对每个数据类型都单独处理的非常好。比如ArrayBuffer什么的。我们都没有处理。
作者:Jesse
链接:https://www.zhihu.com/question/23031215/answer/460652947
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
参考博客:https://www.zhihu.com/question/23031215(讲的比较好,推荐)
深拷贝实现
1、使用递归去复制所有层级属性
function deepClone(obj){
let objClone = Array.isArray(obj)?[]:{
};
if(obj && typeof obj==="object"){
for(key in obj){
if(obj.hasOwnProperty(key)){
//判断obj子元素是否为对象,如果是,递归复制
if(obj[key]&&typeof obj[key] ==="object"){
objClone[key] = deepClone(obj[key]);
}else{
//如果不是,简单复制
objClone[key] = obj[key];
}
}
}
}
return objClone;
}
let a=[1,2,3,4],
b=deepClone(a);
a[0]=2;
console.log('a=',a,'b=',b);
//a= [2, 2, 3, 4] b= [1, 2, 3, 4]
解析:
跟之前想象的一样,现在b脱离了a的控制,不再受a影响了。
这里再次强调,深拷贝,是拷贝对象各个层级的属性
concat方法与slice也存在这样的情况,
他们都不是真正的深拷贝,这里需要注意。
2.除了递归,我们还可以借用 JSON 对象的 parse 和 stringify
function deepClone(obj){
let _obj = JSON.stringify(obj),
objClone = JSON.parse(_obj);
return objClone
}
let a=[0,1,[2,3],4],
b=deepClone(a);
a[0]=1;
a[2][0]=1;
console.log(a,b);
解析:
附带说下,JSON.stringify与JSON.parse除了实现深拷贝,还能结合localStorage实现对象数组存储。有兴趣可以阅读博主这篇文章。
localStorage存储数组,对象,localStorage,sessionStorage存储数组对象:https://www.cnblogs.com/echolun/p/9088189.html
3.除了上面两种方法之外,我们还可以借用JQ的extend方法。
$.extend( [deep ], target, object1 [, objectN ] )
deep表示是否深拷贝,为true为深拷贝,为false,则为浅拷贝
target Object类型 目标对象,其他对象的成员属性将被附加到该对象上。
object1 objectN可选。 Object类型 第一个以及第N个被合并的对象。
let a=[0,1,[2,3],4],
b=$.extend(true,[],a);
a[0]=1;
a[2][0]=1;
console.log(a,b);
方法实现深拷贝,解决循环引用问题
/** * 判断是否是基本数据类型 * @param value */
function isPrimitive(value){
return (typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'symbol' ||
typeof value === 'boolean')
}
/** * 判断是否是一个js对象 * @param value */
function isObject(value){
return Object.prototype.toString.call(value) === "[object Object]"
}
/** * 深拷贝一个值 * @param value */
function cloneDeep(value){
// 记录被拷贝的值,避免循环引用的出现
let memo = {
};
function baseClone(value){
let res;
// 如果是基本数据类型,则直接返回
if(isPrimitive(value)){
return value;
// 如果是引用数据类型,我们浅拷贝一个新值来代替原来的值
}else if(Array.isArray(value)){
res = [...value];
}else if(isObject(value)){
res = {
...value};
}
// 检测我们浅拷贝的这个对象的属性值有没有是引用数据类型。如果是,则递归拷贝
Reflect.ownKeys(res).forEach(key=>{
if(typeof res[key] === "object" && res[key]!== null){
//此处我们用memo来记录已经被拷贝过的引用地址。以此来解决循环引用的问题
if(memo[res[key]]){
res[key] = memo[res[key]];
}else{
memo[res[key]] = res[key];
res[key] = baseClone(res[key])
}
}
})
return res;
}
return baseClone(value)
}
边栏推荐
- Particle dissipation of unity 3D characters
- Successfully solved @keyup Enter= "search()" problems that do not take effect in the El input component
- Makefile implements compilation time statistics
- Mysql八股
- Security-001
- 深入理解TCP协议的连接状态与可靠机制
- 企业数字化转型之传统架构到中台微服务架构
- [激光器原理与应用-5]:激光二极管LD (Laser Diode)与激光二极管驱动器(LD驱动器)
- 开户哪家券商比较好?手机上开户安全吗
- leetcode:146. Least recently used cache by LRU
猜你喜欢
UNIPRO multi terminal deployment to meet customers' diversified needs
Widar3.0复现日志(DFSExtractionCode)
Matrikon OPC simulator tutorial
How to use sublime text to open MD instead of typora
[paper reading | shallow reading] rolx: structural role Extraction & Mining in large graphs
什么是Nacos及实战使用教程
Mysql database and table building (I)
Summary of Digital IC design written examination questions (III)
【LeetCode每日一题——消失的数字】
Compréhension approfondie du protocole TCP
随机推荐
易烊千玺考编,被骂上热搜。。。
Communication project based on NiO mode
CVPR 2022 | 内容感知的文字标志图像生成方法
Opencv learning notes - threshold
1055 The World‘s Richest
什么是Nacos及实战使用教程
C语言------数组
超越Transformer,清华、字节大幅刷新并行文本生成SoTA性能|ICML 2022
Makefile implémente les statistiques de temps de compilation
IEC104 simulator tutorial
Apple and other big factories can't hold anymore! Layoffs and slowing recruitment, "two pronged" plan
程序员成长第二十二篇:如何带团队?
The eighth anniversary of the founding of crowdsource bit and the first anniversary of crowdsource chain officially released the New World Declaration
leetcode:952. Calculate the maximum component size by common factor
Deep learning -- neural network, ML strategy
企业数字化转型之传统架构到中台微服务架构
C语言程序环境和预处理
leetcode:632. 最小区间
Makefile实现编译时间统计
面试必刷TOP101 【链表】