深度克隆从C#/C/Java漫谈到JavaScript真复制
如果只想看js,直接从JavaScript标题开始。
在C#里面,深度clone有System.ICloneable。创建现有实例相同的值创建类的新实例
克隆原理
值类型变量与引用类型变量
如果我们有两个值类型的变量,将其中一个变量的值赋给另一个,实际上会创建该值的一个副本,这个副本与原来的值没有什么关系
——这意味着改变其中一个的值不会影响另一个变量的值。
如果是两个引用类型的变量,其中一个变量的值赋给另一个的话(不包括string类型,CLR会对其有特殊处理),并没有创建值的副本,而是使两个变量执行同一个对象
——这意味着改变对象的值会同时影响两个变量。要真正地创建引用类型的副本,我们必须克隆(clone)变量指向的对象。
C# 深度克隆
实现ICloneable接口使一个类型成为可克隆的(cloneable),这需要提供Clone方法来提供该类型的对象的副本。Clone方法不接受任何参数,返回object类型的对象(不管是何种类型实现该接口)。所以我们获得副本后仍需要进行显式地转换。
实现ICloneable接口的方式取决于我们的类型的数据成员。
如果类型仅包含值类型(int,byte等类型)和string类型的数据成员, 我们只要在Clone方法中初始化一个新的对象,将其的数据成员设置为当前对象的各个成员的值即可。事实上,object类的 MemberwiseClone方法会自动完成该过程。
如果自定义类型包含引用类型的数据成员,必须考虑Clone方法是实现浅拷贝(shallow copy)还是深拷贝(deep copy)。
浅拷贝(shallow copy)是指副本对象中的引用类型的数据成员与源对象的数据成员指向相同的对象。
相当于创建了一个新的对象,只是这个对象的所有内容,都和被拷贝的对象一模一样而已,即两者的修改是隔离的,相互之间没有影响
深拷贝(deep copy)则必须创建整个对象的结构,副本对象中的引用类型的数据成员与源对象的数据成员指向不同的对象。
拷贝者和被拷贝者若是同一个地址,则为浅拷贝,反之为深拷贝。
浅拷贝是容易实现的,就是使用前面提到的MemberwiseClone方法。开发人员往往希望使用的类型能够实现深拷贝,但会发现这样的类型并不 多。这种情况在System.Collections命名空间中尤其常见,这里面的类在其Clone方法中实现的都是浅拷贝。这么做主要出于两个原因:
创建一个大对象的副本对性能影响较大;
通用的集合类型可能会包含各种各样的对象,在这种情况下实现深拷贝并不可行,因为集合中的对象并非都是可克隆的,另外还存在循环引用的情况,这会让深拷贝过程陷入死循环。
C#克隆来自《实现可克隆(Cloneable)的类型》,代码实现参考原文。
C++内存深度克隆
回顾下基础知识,指针和引用主要有以下区别:
引用必须被初始化,但是不分配存储空间。指针不声明时初始化,在初始化的时候需要分配存储空间。
引用初始化后不能被改变,指针可以改变所指的对象。
不存在指向空值的引用,但是存在指向空值的指针——引用不能为空,指针可以为空。
指针指向一块内存,它的内容是所指内存的地址;而引用则是某块内存的别名——指针是一个实体,而引用仅是个别名。
引用没有const,指针有const,const的指针不可变
引用是类型安全的,而指针不是 (引用比指针多了类型检查)
指针和引用的自增(++)运算意义不一样;
引用没有const,指针有const,const的指针不可变;
cont int p 这个p指针不是一个普通的指针,它是个常量指针,即只能对其初始化,而不能赋值
稍微有点c语言基础的人都能看得出深度拷贝和浅拷贝的差异。总而言之,拷贝者和被拷贝者若是同一个地址,则为浅拷贝,反之为深拷贝。
一般的赋值操作是深度拷贝:
//深度拷贝
int a = 5;//在内存中找一块区域,命名为 a,用它来存放整数数据类型 5
int b = a;//在内存中找一块区域,命名为 b,把a拷贝一份,赋给b
char str1 = "HelloWorld";
char str2 = str1;
简单的指针指向,则是浅拷贝:
//浅拷贝
int a = 5;
int *b = &a; //c指向a的地址; &为取地址符,&a就是a这个变量的地址。
int *b; //int *b:定义了一个变量b,它是指针型的,关联数据类型 为int.
b = &a; //int *b=&a表示b指针所指向的数据,等于a的地址. int *b =a 表示b指针指向a,即把a赋值给*b;
// *a=b表示a指针所指向的数据,等于b。*a=&b表示a指针所指向的数据,等于b的地址du。 char* str1 = "HelloWorld";
char* str2 = str1;
将上面的浅拷贝改为深度拷贝后:
//深度拷贝
int a = 8;
int *p = new int;//new int(a)
*p = a;
char* str1 = "HelloWorld";
int len = strlen(str1);
char *str2 = new char[len];
memcpy(str2, str1, len);
以字符串拷贝为例
浅拷贝后,str1和str2同指向0x123456,不管哪一个指针,对该空间内容的修改都会影响另一个指针。
str1和str2指向不同的内存空间,各自的空间的内容一样。因为空间不同,所以不管哪一个指针,对该空间内容的修改都不会影响另一个指针。


在某些状况下,类内成员变量需要动态开辟堆内存,如果实行位拷贝,也就是把对象里的值完全复制给另一个对象,如A=B。这时,如果B中有一个成员变量指针已经申请了内存,那A中的那个成员变量也指向同一块内存。这就出现了问题:当B把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误。
深拷贝和浅拷贝可以简单理解为:如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝。
深拷贝和浅拷贝的定义可以简单理解成:如果一个类拥有资源(堆,或者是其它系统资源),当这个类的对象发生复制过程的时候,这个过程就可以叫做深拷贝,反之对象存在资源,但复制过程并未复制资源的情况视为浅拷贝。
浅拷贝资源后在释放资源的时候会产生资源归属不清的情况导致程序运行出错。
IplImage *p1 = cvLoadImage( "Lena.jpg" );
IplImage *p2 = p1;
p1 = NULL ;//or cvReleaseImage(p1);释放图像
以下的思考不知对不对——编程小翁
IplImage *是OpenCV里面的东西,它代表一张图。经过第二句后,p1与p2指向相同的对象,在底层就是指向同一块内存块。问题就来了,在第三句执行完毕后,p2还指向原来的对象吗?调试表明,YES。以前一直纠结着,p1都被置为空了(NULL),那原来的对象是不是也跟着被销毁了?其实,错了。
首先,我们应该把指针与其所指的对象分开看。指针重定向或者被置为NULL,对于其原先所指的对象的没有影响的。(但其实,应该会造成内存泄露,因为如果没有其他指针“接管”这部分内存块,就成无名的内存块摆在那边了,也就无法释放掉) 在p1重定向后,p2仍旧指向原来的对象。在此刻,p1与p2其实就是两个无关的事务了,也就是“分家”了。
java 深度克隆
java深度拷贝一般都用分装好的工具。没有必要重复造轮子。apache和spring都提供了BeanUtils的深度拷贝工具包。
把对象写到流里的过程是串行化(Serilization)过程,但是在Java程序师圈子里又非常形象地称为“冷冻”或者“腌咸菜(picking)”过程;而把对象从流中读出来的并行化(Deserialization)过程则叫做“解冻”或者“回鲜(depicking)”过程。应当指出的是,写在流里的是对象的一个拷贝,而原对象仍然存在于JVM里面,因此“腌成咸菜”的只是对象的一个拷贝,Java咸菜还可以回鲜。
在Java语言里深复制一个对象,常常可以先使对象实现Serializable接口,然后把对象(实际上只是对象的一个拷贝)写到一个流里(腌成咸菜),再从流里读出来(把咸菜回鲜),便可以重建对象。
在项目中我们需要克隆的对象可能包含多层引用类型,这就要涉及到多层克隆问题,多层克隆不仅要将克隆对象实现序列化接口,引用对象也同样的要实现序列化接口:
翻看JDK源码,Object类里面的clone方法定义如下
protected native Object clone() throws CloneNotSupportedException;
是“bitwise(逐位)的复制, 将该对象的内存空间完全复制到新的空间中去”这样实现的。
JavaScript深度拷贝
JavaScript深度克隆,首先想到是JSON.parse(JSON.stringify(target)),但是
JSON 克隆不支持函数、引用、undefined、Date、RegExp 等
递归克隆要考虑环、爆栈
要考虑 Date、RegExp、Function 等特殊对象的克隆方式
要不要克隆 __proto__,如果要克隆,就非常浪费内存;如果不克隆,就不是深克隆。
循环引用如何深度克隆
JSON.parse(JSON.stringify(target))数据及结构丢失
JSON.stringify() 将值转换为相应的JSON格式:
转换值如果有 toJSON() 方法,该方法定义什么值将被序列化。Date 日期调用了 toJSON() 将其转换为了 string 字符串(同Date.toISOString()),因此会被当做字符串处理。
布尔值、数字、字符串的包装对象在序列化过程中会自动转换成对应的原始值。
非数组对象的属性不能保证以特定的顺序出现在序列化后的字符串中。
undefined和null、任意的函数、正则表达式、symbol 值、NaN 和 Infinity 等,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时)。函数、undefined 被单独转换时,会返回 undefined,如JSON.stringify(function(){}) or JSON.stringify(undefined)。NaN 和 Infinity 格式的数值及 null 都会被当做 null。正则转换为空对象。
对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误。
所有以 symbol 为属性键的属性都会被完全忽略掉,即便 replacer 参数中强制指定包含了它们。
其他类型的对象,包括 Map/Set/WeakMap/WeakSet,仅会序列化可枚举的属性。
循环引用的对象使用 JSON.stringify 为什么会报错
let obj1={},obj2={};
obj1.a = obj2;
obj2.b = obj1;
结果就是 。obj1.a.b.a.b.a.b.a.b.a.b.a……………………无限循环引用
obj1 这个对象和 obj2 会无限相互引用,JSON.tostringify 无法将一个无限引用的对象序列化为 JOSN 字符串。
目前几乎所有的直接深复制对象的都有这样那样的问题 都不是很完美,但实际工作中需要用到完美深复制对象的场景也少之又少,包括jquery提供的extend方法也由于考虑到内存占用问题 在多层嵌套的数据里捉襟见肘。所以我们很多时候需要定制 clone 函数
一般手写的克隆函数都是这个样子
function clone(Obj) {
var buf;
if (Obj instanceof Array) {
buf = [];
//创建一个空的数组
var i = Obj.length;
while (i--) {
buf[i] = clone(Obj[i]);
}
return buf;
} else if (Obj instanceof Object) {
buf = {};
//创建一个空对象
for (var k in Obj) {
//为这个对象添加新的属性
buf[k] = clone(Obj[k]);
}
return buf;
} else {
//普通变量直接赋值
return Obj;
}
}
精炼下
// 方法一:
function clone (obj) {
if (typeof obj !== 'object') return false
var o = obj.constructor === Array ? [] : {};
for (var e in this) {
o[e] = typeof this[e] === "object" ? this[e].clone() : this[e];
}
return o;
};
增加判断类型
switch (Object.prototype.toString.call(obj).toLowerCase()) {
case '[object Array]':
// clone array
break
case '[object Object]':
// clone object
break
case '[object Date]':
return new Date(obj)
break
case '[object RegExp]':
retrun =new RegExp(obj)
/* let flags = ''
if (obj.global) flags += 'g'
if (obj.ignoreCase) flags += 'i'
if (obj.multiline) flags += 'm' */
// ***
break
case '[object HTMLBodyElement]':
// Dom Element clone,// 遍历Dom树,每个节点 cloneNode(true),个人觉得没有必要。
return obj.cloneNode(true)
case '[object Function]':
// new function inherit && extent obj
// return (new obj()).constructor;
break
case '[object Symbol]':
// Symbol 既然定义为唯一的。那么久没有所谓的复制
throw new Error('')
// JavaScript 各种内置对象 类型太多了。不能入戏太深
default:
return obj
}
其实这个只是造火箭面试的一个考核。实际就是数据复制而已。但是,比较处理循环引用是重点。
解决循环引用的方案探讨
循环引用的问题关键就是 obj1.a.b.a.b.a.b.a.b.a.b.a……………………无限循环引用,溢出问题。
WeakMap解决循环引用死循环
WeakMap 其中的键是弱引用的。其键必须是对象,而值可以是任意的(我一般用此来缓存计算结果,参考java中利用WeakHashMap实现缓存)。
const deepClone = (obj, hash=new WeakMap) => {
let data = new obj.constructor();
// 取出循环引用
if(hash.get(obj)) return hash.get(obj)
hash.set(obj, data);
for(var k in obj) {
if(obj.hasOwnProperty(k)){
data[k] = deepClone(obj[k], hash);
}
}
return obj;
}
WeakMap 健弱引用,帮助我们解决问题。
使用Array循环引用死循环
function deepClone(source,uniqueList=[]){
// determineUnique
if(determineIteration){
return uniqueData.target;
}
uniqueList.push({source:source,target:target});
//TODO deep clone
}
function determineIteration(uniqueList,target){
retrun uniqueList.find(item=>item.source===target)
}
deepClone始终有性能问题,如果业务层(大概率)是担心修改引用数据,使用immutable库或者immer库才是解决问题的正路。
目前使用较多还是 lodash deepclone
参考文章:
实现可克隆(Cloneable)的类型 https://www.cnblogs.com/anderslly/archive/2007/04/08/implementingcloneabletype.html
ICloneable 的方法实现 不要轻易使用ICloneable https://blog.csdn.net/iteye_14608/article/details/82404997
关于c中int a=1; int b=a类型问题的思考 https://www.cnblogs.com/wengzilin/archive/2013/03/25/2980520.html
转载本站文章《深度克隆从C#/C/Java漫谈到JavaScript真复制》,
请注明出处:https://www.zhoulujun.cn/html/webfront/ECMAScript/js6/2018_1219_8450.html
深度克隆从C#/C/Java漫谈到JavaScript真复制的更多相关文章
- js对象的深度克隆
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
- Java List的深度克隆
关于java List的深度克隆 List是java容器中最常用的顺序存储数据结构之一.有些时候我们将一组数据取出放到一个List对象中,但是可能会很多处程序要读取他或者是修改他.尤其是并发处理的话, ...
- java中传值及引伸深度克隆的思考(说白了Java只能传递对象指针)
java中传值及引伸深度克隆的思考 大家都知道java中没有指针.难道java真的没有指针吗?句柄是什么?变量地址在哪里?没有地址的话简直不可想象! java中内存的分配方式有两种,一种是在堆中分配, ...
- java对象 深度克隆(不实现Cloneable接口)和浅度克隆
详见:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcyt128 为什么需要克隆: 在实际编程过程中,我们常常要遇到这种情况:有一个对象 ...
- 如何复制一个java对象(浅克隆与深度克隆)
在项目中,有时候有一些比较重要的对象经常被当作参数传来传去,和C语言的值传递不同,java语言的传递都是引用传递,在任何一个地方修改了这个对象的值,就会导致这个对象在内存中的值被彻底改变.但是很多时候 ...
- Java中深度克隆和浅度克隆
一:使用目的: 就是为了快速构造一个和已有对象相同的副本.如果需要克隆对象,一般需要先创建一个对象,然后将原对象中的数据导入到新创建的对象中去,而不用根据已有对象进行手动赋值操作. 二:Object中 ...
- Java的深度克隆和浅度克隆
说到克隆,其实是个比较简单的概念,跟现实生活正的克隆一样,复制一个一模一样的对象出来.clone()这个方法是从Object继承下来的,一个对象要实现克隆,需要实现一个叫做Cloneable的接口,这 ...
- Java的赋值、浅克隆和深度克隆的区别
赋值 直接 = ,克隆 clone 假如说你想复制一个简单变量.很简单: int a= 5; int b= a; b = 6; 这样 a == 5, b == 6 不仅仅是int类型,其它七种原始数 ...
- JAVA对象的深度克隆
有时候,我们需要把对象A的所有值复制给对象B(B = A),但是这样用等号给赋值你会发现,当B中的某个对象值改变时,同时也会修改到A中相应对象的值! 也许你会说,用clone()不就行了?!你的想法只 ...
- 原型模式 —— Java的赋值、浅克隆和深度克隆的区别
赋值 直接 = ,克隆 clone 假如说你想复制一个简单变量.很简单: int a= 5; int b= a; b = 6; 这样 a == 5, b == 6 不仅仅是int类型,其它七种原始数 ...
随机推荐
- python实现汉诺塔的图解递归算法
写在前面 工作闲来无事,看了python,写了一个汉诺塔. 还是蛮喜欢python这门语言的,很简洁. 正文 一.起源: 汉诺塔(又称河内塔)问题是源于印度一个古老传说的益智玩具.大梵天创造世界的时候 ...
- Util应用框架核心(三) - 服务注册器
本节介绍服务注册器的开发. 如果你不需要扩展Util应用框架,直接跳过. 当你把某些功能封装到自己的类库,并希望启动时自动执行初始化代码进行配置时,定义服务注册器. 服务注册器概述 服务注册器是Uti ...
- animate.css 动画种类(详细)
作者:WangMin 格言:努力做好自己喜欢的每一件事 以下为各种动画类型包含的不同动画效果类,仅供参考.具体可查看animate.css 官网. bounce 弹跳 2. flash 闪烁 3. p ...
- "拍牌神器"是怎样炼成的(三)---注册全局热键
要想在上海拍牌的超低中标率中把握机会.占得先机,您不仅需要事先准备好最优的竞拍策略,还要制定若干套应急预案,应对不时之需.既定策略交给计算机自动执行,没有问题.可是谁来召唤应急预案呢?使用全局热键应该 ...
- baby_web
点开页面获得提示 根据提示,访问index.php,但是会自己跳转到1.php 这时候抓包修改才ok才能定位到index.php
- WebGL初接触
有感于在不少jd中看到关于WebGL的内容,想起来之前在高程中还没看完的canvas,就匆匆把剩余的一点看完了,高程中的内容还是皮毛,就属于很基础的.概念性的东西. WebGL 画布的3D上下文.不是 ...
- Codeforces Round #697 (Div. 3) A~E题解
写在前边 状态及其不佳,很累很困,还好\(unrated\)了 链接:Codeforces Round #697 (Div. 3) A. Odd Divisor 链接:A题链接 题目大意: 判断一个数 ...
- Java笔记——数组静态初始化开始
一维数组: 静态初始化: 定义格式:(1)数据类型[] 数组名 = new 数组类型[] (2)数组类型[] 数组名 ={元素1,元素2,.....} 练习:数组元素逆序: public static ...
- 华为云WebAssembly代码静态符号执行技术实现新突破
本文分享自华为云社区<华为云WebAssembly代码静态符号执行技术实现新突破,相关论文被软件工程顶会ISSTA2023接收并荣获杰出论文奖>,作者:华为云软件分析Lab . WebAs ...
- Chrome扩展的核心:manifest 文件(上)
大家好,我是dom哥.我正在写关于 Chrome 扩展开发的系列文章,感兴趣的可以点个小星星. Chrome 在全球浏览器市场份额独占 6 成,无论是对普通用户还是开发者,都是电脑里的必备利器.Chr ...