如果只想看js匙睹,直接從JavaScript標題開始。
在C#里面济竹,深度clone有System.ICloneable痕檬。創(chuàng)建現(xiàn)有實例相同的值創(chuàng)建類的新實例
克隆原理
值類型變量與引用類型變量
如果我們有兩個值類型的變量,將其中一個變量的值賦給另一個送浊,實際上會創(chuàng)建該值的一個副本梦谜,這個副本與原來的值沒有什么關(guān)系
——這意味著改變其中一個的值不會影響另一個變量的值。
如果是兩個引用類型的變量袭景,其中一個變量的值賦給另一個的話(不包括string類型唁桩,CLR會對其有特殊處理),并沒有創(chuàng)建值的副本耸棒,而是使兩個變量執(zhí)行同一個對象
——這意味著改變對象的值會同時影響兩個變量荒澡。要真正地創(chuàng)建引用類型的副本,我們必須克掠胙辍(clone)變量指向的對象单山。
C# 深度克隆
實現(xiàn)ICloneable接口使一個類型成為可克隆的(cloneable),這需要提供Clone方法來提供該類型的對象的副本幅疼。Clone方法不接受任何參數(shù)米奸,返回object類型的對象(不管是何種類型實現(xiàn)該接口)。所以我們獲得副本后仍需要進行顯式地轉(zhuǎn)換爽篷。
實現(xiàn)ICloneable接口的方式取決于我們的類型的數(shù)據(jù)成員悴晰。
如果類型僅包含值類型(int,byte等類型)和string類型的數(shù)據(jù)成員逐工, 我們只要在Clone方法中初始化一個新的對象铡溪,將其的數(shù)據(jù)成員設(shè)置為當前對象的各個成員的值即可。事實上泪喊,object類的 MemberwiseClone方法會自動完成該過程棕硫。
如果自定義類型包含引用類型的數(shù)據(jù)成員,必須考慮Clone方法是實現(xiàn)淺拷貝(shallow copy)還是深拷貝(deep copy)窘俺。
淺拷貝(shallow copy)是指副本對象中的引用類型的數(shù)據(jù)成員與源對象的數(shù)據(jù)成員指向相同的對象饲帅。
相當于創(chuàng)建了一個新的對象,只是這個對象的所有內(nèi)容瘤泪,都和被拷貝的對象一模一樣而已灶泵,即兩者的修改是隔離的,相互之間沒有影響
深拷貝(deep copy)則必須創(chuàng)建整個對象的結(jié)構(gòu)对途,副本對象中的引用類型的數(shù)據(jù)成員與源對象的數(shù)據(jù)成員指向不同的對象赦邻。
拷貝者和被拷貝者若是同一個地址,則為淺拷貝实檀,反之為深拷貝惶洲。
淺拷貝是容易實現(xiàn)的,就是使用前面提到的MemberwiseClone方法膳犹。開發(fā)人員往往希望使用的類型能夠?qū)崿F(xiàn)深拷貝恬吕,但會發(fā)現(xiàn)這樣的類型并不 多。這種情況在System.Collections命名空間中尤其常見须床,這里面的類在其Clone方法中實現(xiàn)的都是淺拷貝铐料。這么做主要出于兩個原因:
創(chuàng)建一個大對象的副本對性能影響較大;
通用的集合類型可能會包含各種各樣的對象豺旬,在這種情況下實現(xiàn)深拷貝并不可行钠惩,因為集合中的對象并非都是可克隆的,另外還存在循環(huán)引用的情況族阅,這會讓深拷貝過程陷入死循環(huán)篓跛。
C#克隆來自《實現(xiàn)可克隆(Cloneable)的類型》坦刀,代碼實現(xiàn)參考原文愧沟。
C++內(nèi)存深度克隆
回顧下基礎(chǔ)知識,指針和引用主要有以下區(qū)別:
引用必須被初始化鲤遥,但是不分配存儲空間央渣。指針不聲明時初始化,在初始化的時候需要分配存儲空間渴频。
引用初始化后不能被改變芽丹,指針可以改變所指的對象。
不存在指向空值的引用卜朗,但是存在指向空值的指針——引用不能為空拔第,指針可以為空。
指針指向一塊內(nèi)存场钉,它的內(nèi)容是所指內(nèi)存的地址蚊俺;而引用則是某塊內(nèi)存的別名——指針是一個實體,而引用僅是個別名逛万。
引用沒有const泳猬,指針有const,const的指針不可變
引用是類型安全的,而指針不是 (引用比指針多了類型檢查)
指針和引用的自增(++)運算意義不一樣得封;
引用沒有const埋心,指針有const,const的指針不可變忙上;
cont int p 這個p指針不是一個普通的指針拷呆,它是個常量指針,即只能對其初始化疫粥,而不能賦值
稍微有點c語言基礎(chǔ)的人都能看得出深度拷貝和淺拷貝的差異茬斧。總而言之梗逮,拷貝者和被拷貝者若是同一個地址项秉,則為淺拷貝,反之為深拷貝慷彤。
?一般的賦值操作是深度拷貝:
//深度拷貝
int?a?=?5;//在內(nèi)存中找一塊區(qū)域伙狐,命名為?a,用它來存放整數(shù)數(shù)據(jù)類型?5
int?b?=?a;//在內(nèi)存中找一塊區(qū)域瞬欧,命名為?b贷屎,把a拷貝一份,賦給b
char?str1?=?"HelloWorld";
char?str2?=?str1;
簡單的指針指向艘虎,則是淺拷貝:
//淺拷貝
int?a?=?5;
int?*b?=?&a;?//c指向a的地址;?&為取地址符唉侄,&a就是a這個變量的地址。
int?*b;?//int?*b:定義了一個變量b野建,它是指針型的属划,關(guān)聯(lián)數(shù)據(jù)類型?為int.
b?=?&a;?//int?*b=&a表示b指針所指向的數(shù)據(jù),等于a的地址.?int?*b?=a?表示b指針指向a,即把a賦值給*b候生;
//?*a=b表示a指針所指向的數(shù)據(jù)同眯,等于b。*a=&b表示a指針所指向的數(shù)據(jù)唯鸭,等于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目溉,不管哪一個指針明肮,對該空間內(nèi)容的修改都會影響另一個指針。
str1和str2指向不同的內(nèi)存空間缭付,各自的空間的內(nèi)容一樣柿估。因為空間不同,所以不管哪一個指針陷猫,對該空間內(nèi)容的修改都不會影響另一個指針秫舌。
在某些狀況下的妖,類內(nèi)成員變量需要動態(tài)開辟堆內(nèi)存,如果實行位拷貝足陨,也就是把對象里的值完全復制給另一個對象嫂粟,如A=B丁逝。這時全景,如果B中有一個成員變量指針已經(jīng)申請了內(nèi)存综芥,那A中的那個成員變量也指向同一塊內(nèi)存。這就出現(xiàn)了問題:當B把內(nèi)存釋放了(如:析構(gòu))飒房,這時A內(nèi)的指針就是野指針了,出現(xiàn)運行錯誤媚值。
深拷貝和淺拷貝可以簡單理解為:如果一個類擁有資源狠毯,當這個類的對象發(fā)生復制過程的時候,資源重新分配褥芒,這個過程就是深拷貝嚼松,反之,沒有重新分配資源锰扶,就是淺拷貝献酗。
深拷貝和淺拷貝的定義可以簡單理解成:如果一個類擁有資源(堆,或者是其它系統(tǒng)資源)坷牛,當這個類的對象發(fā)生復制過程的時候罕偎,這個過程就可以叫做深拷貝,反之對象存在資源京闰,但復制過程并未復制資源的情況視為淺拷貝颜及。
淺拷貝資源后在釋放資源的時候會產(chǎn)生資源歸屬不清的情況導致程序運行出錯。
IplImage?*p1?=?cvLoadImage(?"Lena.jpg"?);
IplImage?*p2?=?p1;
p1?=?NULL?;//or?cvReleaseImage(p1);釋放圖像
以下的思考不知對不對——編程小翁
IplImage *是OpenCV里面的東西蹂楣,它代表一張圖俏站。經(jīng)過第二句后,p1與p2指向相同的對象痊土,在底層就是指向同一塊內(nèi)存塊肄扎。問題就來了,在第三句執(zhí)行完畢后赁酝,p2還指向原來的對象嗎反浓?調(diào)試表明,YES赞哗。以前一直糾結(jié)著雷则,p1都被置為空了(NULL),那原來的對象是不是也跟著被銷毀了肪笋?其實月劈,錯了度迂。
首先,我們應該把指針與其所指的對象分開看猜揪。指針重定向或者被置為NULL惭墓,對于其原先所指的對象的沒有影響的。(但其實而姐,應該會造成內(nèi)存泄露腊凶,因為如果沒有其他指針“接管”這部分內(nèi)存塊,就成無名的內(nèi)存塊擺在那邊了拴念,也就無法釋放掉)? 在p1重定向后钧萍,p2仍舊指向原來的對象。在此刻政鼠,p1與p2其實就是兩個無關(guān)的事務了风瘦,也就是“分家”了。
java 深度克隆
java深度拷貝一般都用分裝好的工具公般。沒有必要重復造輪子万搔。apache和spring都提供了BeanUtils的深度拷貝工具包。
把對象寫到流里的過程是串行化(Serilization)過程官帘,但是在Java程序師圈子里又非常形象地稱為“冷凍”或者“腌咸菜(picking)”過程瞬雹;而把對象從流中讀出來的并行化(Deserialization)過程則叫做“解凍”或者“回鮮(depicking)”過程。應當指出的是刽虹,寫在流里的是對象的一個拷貝挖炬,而原對象仍然存在于JVM里面,因此“腌成咸菜”的只是對象的一個拷貝状婶,Java咸菜還可以回鮮意敛。
在Java語言里深復制一個對象,常程懦妫可以先使對象實現(xiàn)Serializable接口草姻,然后把對象(實際上只是對象的一個拷貝)寫到一個流里(腌成咸菜),再從流里讀出來(把咸菜回鮮)稍刀,便可以重建對象撩独。
在項目中我們需要克隆的對象可能包含多層引用類型,這就要涉及到多層克隆問題账月,多層克隆不僅要將克隆對象實現(xiàn)序列化接口综膀,引用對象也同樣的要實現(xiàn)序列化接口:
翻看JDK源碼,Object類里面的clone方法定義如下
protected native Object clone() throws CloneNotSupportedException;
是“bitwise(逐位)的復制局齿, 將該對象的內(nèi)存空間完全復制到新的空間中去”這樣實現(xiàn)的剧劝。
JavaScript深度拷貝
JavaScript深度克隆,首先想到是JSON.parse(JSON.stringify(target))抓歼,但是
JSON 克隆不支持函數(shù)讥此、引用拢锹、undefined、Date萄喳、RegExp 等
遞歸克隆要考慮環(huán)卒稳、爆棧
要考慮 Date、RegExp他巨、Function 等特殊對象的克隆方式
要不要克隆 __proto__充坑,如果要克隆,就非常浪費內(nèi)存染突;如果不克隆捻爷,就不是深克隆。
循環(huán)引用如何深度克隆
JSON.parse(JSON.stringify(target))數(shù)據(jù)及結(jié)構(gòu)丟失
JSON.stringify() 將值轉(zhuǎn)換為相應的JSON格式:
轉(zhuǎn)換值如果有 toJSON() 方法觉痛,該方法定義什么值將被序列化役衡。Date 日期調(diào)用了 toJSON() 將其轉(zhuǎn)換為了 string 字符串(同Date.toISOString())茵休,因此會被當做字符串處理薪棒。
布爾值、數(shù)字榕莺、字符串的包裝對象在序列化過程中會自動轉(zhuǎn)換成對應的原始值俐芯。
非數(shù)組對象的屬性不能保證以特定的順序出現(xiàn)在序列化后的字符串中。
undefined和null钉鸯、任意的函數(shù)吧史、正則表達式、symbol 值唠雕、NaN 和 Infinity 等贸营,在序列化過程中會被忽略(出現(xiàn)在非數(shù)組對象的屬性值中時)或者被轉(zhuǎn)換成 null(出現(xiàn)在數(shù)組中時)。函數(shù)岩睁、undefined 被單獨轉(zhuǎn)換時钞脂,會返回 undefined,如JSON.stringify(function(){}) or JSON.stringify(undefined)捕儒。NaN 和 Infinity 格式的數(shù)值及 null 都會被當做 null冰啃。正則轉(zhuǎn)換為空對象。
對包含循環(huán)引用的對象(對象之間相互引用刘莹,形成無限循環(huán))執(zhí)行此方法阎毅,會拋出錯誤。
所有以 symbol 為屬性鍵的屬性都會被完全忽略掉点弯,即便 replacer 參數(shù)中強制指定包含了它們扇调。
其他類型的對象,包括?Map/Set/WeakMap/WeakSet抢肛,僅會序列化可枚舉的屬性肃拜。
循環(huán)引用的對象使用 JSON.stringify 為什么會報錯
let obj1={},obj2={};
obj1.a = obj2;
obj2.b = obj1;
結(jié)果就是 痴腌。obj1.a.b.a.b.a.b.a.b.a.b.a……………………無限循環(huán)引用
obj1 這個對象和 obj2 會無限相互引用,JSON.tostringify 無法將一個無限引用的對象序列化為 JOSN 字符串燃领。
目前幾乎所有的直接深復制對象的都有這樣那樣的問題 都不是很完美士聪,但實際工作中需要用到完美深復制對象的場景也少之又少,包括jquery提供的extend方法也由于考慮到內(nèi)存占用問題 在多層嵌套的數(shù)據(jù)里捉襟見肘猛蔽。所以我們很多時候需要定制 clone 函數(shù)
一般手寫的克隆函數(shù)都是這個樣子
function?clone(Obj)?{
????var?buf;
????if?(Obj?instanceof?Array)?{
????????buf?=?[];
????????//創(chuàng)建一個空的數(shù)組
????????var?i?=?Obj.length;
????????while?(i--)?{
????????????buf[i]?=?clone(Obj[i]);
????????}
????????return?buf;
????}?else?if?(Obj?instanceof?Object)?{
????????buf?=?{};
????????//創(chuàng)建一個空對象
????????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樹,每個節(jié)點?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?各種內(nèi)置對象?類型太多了毁枯。不能入戲太深
??default:
????return?obj
}
其實這個只是造火箭面試的一個考核慈缔。實際就是數(shù)據(jù)復制而已。但是种玛,比較處理循環(huán)引用是重點藐鹤。
解決循環(huán)引用的方案探討
循環(huán)引用的問題關(guān)鍵就是?obj1.a.b.a.b.a.b.a.b.a.b.a……………………無限循環(huán)引用,溢出問題赂韵。
WeakMap解決循環(huán)引用死循環(huán)
WeakMap?其中的鍵是弱引用的娱节。其鍵必須是對象,而值可以是任意的(我一般用此來緩存計算結(jié)果祭示,參考java中利用WeakHashMap實現(xiàn)緩存)肄满。
const?deepClone?=?(obj,?hash=new?WeakMap)?=>?{
????let?data?=?new?obj.constructor();
????//?取出循環(huán)引用
????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循環(huán)引用死循環(huán)
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始終有性能問題稠歉,如果業(yè)務層(大概率)是擔心修改引用數(shù)據(jù),使用immutable庫或者immer庫才是解決問題的正路汇陆。
目前使用較多還是 lodash?deepclone
參考文章:
實現(xiàn)可克屡ā(Cloneable)的類型?https://www.cnblogs.com/anderslly/archive/2007/04/08/implementingcloneabletype.html
ICloneable 的方法實現(xiàn) 不要輕易使用ICloneable?https://blog.csdn.net/iteye_14608/article/details/82404997
關(guān)于c中int a=1; int b=a類型問題的思考?https://www.cnblogs.com/wengzilin/archive/2013/03/25/2980520.html
轉(zhuǎn)載本站文章《深度克隆從C#/C/Java漫談到JavaScript真復制》,
請注明出處:https://www.zhoulujun.cn/html/webfront/ECMAScript/js6/2018_1219_8450.html