深度克隆從C#/C/Java漫談到JavaScript真復制

如果只想看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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市瞬测,隨后出現(xiàn)的幾起案子横媚,更是在濱河造成了極大的恐慌,老刑警劉巖月趟,帶你破解...
    沈念sama閱讀 211,265評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件灯蝴,死亡現(xiàn)場離奇詭異,居然都是意外死亡孝宗,警方通過查閱死者的電腦和手機穷躁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評論 2 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人问潭,你說我怎么就攤上這事猿诸。” “怎么了狡忙?”我有些...
    開封第一講書人閱讀 156,852評論 0 347
  • 文/不壞的土叔 我叫張陵梳虽,是天一觀的道長。 經(jīng)常有香客問我灾茁,道長窜觉,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,408評論 1 283
  • 正文 為了忘掉前任北专,我火速辦了婚禮禀挫,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘拓颓。我一直安慰自己语婴,他們只是感情好,可當我...
    茶點故事閱讀 65,445評論 5 384
  • 文/花漫 我一把揭開白布驶睦。 她就那樣靜靜地躺著砰左,像睡著了一般。 火紅的嫁衣襯著肌膚如雪啥繁。 梳的紋絲不亂的頭發(fā)上菜职,一...
    開封第一講書人閱讀 49,772評論 1 290
  • 那天青抛,我揣著相機與錄音旗闽,去河邊找鬼。 笑死蜜另,一個胖子當著我的面吹牛适室,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播举瑰,決...
    沈念sama閱讀 38,921評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼捣辆,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了此迅?” 一聲冷哼從身側(cè)響起汽畴,我...
    開封第一講書人閱讀 37,688評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎耸序,沒想到半個月后忍些,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,130評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡坎怪,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,467評論 2 325
  • 正文 我和宋清朗相戀三年罢坝,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片搅窿。...
    茶點故事閱讀 38,617評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡嘁酿,死狀恐怖隙券,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情闹司,我是刑警寧澤娱仔,帶...
    沈念sama閱讀 34,276評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站游桩,受9級特大地震影響拟枚,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜众弓,卻給世界環(huán)境...
    茶點故事閱讀 39,882評論 3 312
  • 文/蒙蒙 一恩溅、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧谓娃,春花似錦脚乡、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至捡遍,卻和暖如春锌订,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背画株。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評論 1 265
  • 我被黑心中介騙來泰國打工辆飘, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人谓传。 一個月前我還...
    沈念sama閱讀 46,315評論 2 360
  • 正文 我出身青樓蜈项,卻偏偏與公主長得像,于是被迫代替她去往敵國和親续挟。 傳聞我的和親對象是個殘疾皇子紧卒,可洞房花燭夜當晚...
    茶點故事閱讀 43,486評論 2 348