2024-02-23【技術(shù)】淺拷貝的原理與實(shí)現(xiàn)(下)

深拷貝的原理和實(shí)現(xiàn)

淺拷貝只是創(chuàng)建了一個(gè)新的對(duì)象卿吐,復(fù)制了原有對(duì)象的基本類型的值旁舰,而引用數(shù)據(jù)類型只拷貝了一層屬性,再深層的還是無(wú)法進(jìn)行拷貝嗡官。深拷貝則不同箭窜,對(duì)于復(fù)雜引用數(shù)據(jù)類型,其在堆內(nèi)存中完全開(kāi)辟了一塊內(nèi)存地址衍腥,并將原有的對(duì)象完全復(fù)制過(guò)來(lái)存放磺樱。

這兩個(gè)對(duì)象是相互獨(dú)立、不受影響的紧阔,徹底實(shí)現(xiàn)了內(nèi)存上的分離坊罢。總的來(lái)說(shuō)擅耽,深拷貝的原理可以總結(jié)如下:

將一個(gè)對(duì)象從內(nèi)存中完整地拷貝出來(lái)一份給目標(biāo)對(duì)象,并從堆內(nèi)存中開(kāi)辟一個(gè)全新的空間存放新對(duì)象物遇,且新對(duì)象的修改并不會(huì)改變?cè)瓕?duì)象乖仇,二者實(shí)現(xiàn)真正的分離。

現(xiàn)在原理你知道了询兴,那么怎么去實(shí)現(xiàn)深拷貝呢乃沙?我也總結(jié)了幾種方法分享給你。

方法一:乞丐版(JSON.stringify)

JSON.stringify() 是目前開(kāi)發(fā)過(guò)程中最簡(jiǎn)單的深拷貝方法诗舰,其實(shí)就是把一個(gè)對(duì)象序列化成為 JSON 的字符串警儒,并將對(duì)象里面的內(nèi)容轉(zhuǎn)換成字符串,最后再用 JSON.parse() 的方法將JSON 字符串生成一個(gè)新的對(duì)象。示例代碼如下所示蜀铲。

let obj1 = { a:1, b:[1,2,3] }
let str = JSON.stringify(obj1)边琉;
let obj2 = JSON.parse(str);
console.log(obj2);   //{a:1,b:[1,2,3]} 
obj1.a = 2记劝;
obj1.b.push(4);
console.log(obj1);   //{a:2,b:[1,2,3,4]}
console.log(obj2);   //{a:1,b:[1,2,3]}

從上面的代碼可以看到变姨,通過(guò) JSON.stringify 可以初步實(shí)現(xiàn)一個(gè)對(duì)象的深拷貝,通過(guò)改變 obj1 的 b 屬性厌丑,其實(shí)可以看出 obj2 這個(gè)對(duì)象也不受影響定欧。

但是使用 JSON.stringify 實(shí)現(xiàn)深拷貝還是有一些地方值得注意,我總結(jié)下來(lái)主要有這幾點(diǎn):

  • 拷貝的對(duì)象的值中如果有函數(shù)怒竿、undefined砍鸠、symbol 這幾種類型,經(jīng)過(guò) JSON.stringify 序列化之后的字符串中這個(gè)鍵值對(duì)會(huì)消失耕驰;
  • 拷貝 Date 引用類型會(huì)變成字符串爷辱;
  • 無(wú)法拷貝不可枚舉的屬性;
  • 無(wú)法拷貝對(duì)象的原型鏈耍属;
  • 拷貝 RegExp 引用類型會(huì)變成空對(duì)象托嚣;
  • 對(duì)象中含有 NaN、Infinity 以及 -Infinity厚骗,JSON 序列化的結(jié)果會(huì)變成 null示启;
  • 無(wú)法拷貝對(duì)象的循環(huán)應(yīng)用,即對(duì)象成環(huán) (obj[key] = obj)领舰。

針對(duì)這些存在的問(wèn)題夫嗓,你可以嘗試著用下面的這段代碼親自執(zhí)行一遍,來(lái)看看如此復(fù)雜的對(duì)象冲秽,如果用 JSON.stringify 實(shí)現(xiàn)深拷貝會(huì)出現(xiàn)什么情況舍咖。

function Obj() { 
  this.func = function () { alert(1) }; 
  this.obj = {a:1};
  this.arr = [1,2,3];
  this.und = undefined; 
  this.reg = /123/; 
  this.date = new Date(0); 
  this.NaN = NaN;
  this.infinity = Infinity;
  this.sym = Symbol(1);
} 

let obj1 = new Obj();

Object.defineProperty(obj1,'innumerable',{ 
  enumerable:false,
  value:'innumerable'
});

console.log('obj1',obj1);
let str = JSON.stringify(obj1);
let obj2 = JSON.parse(str);
console.log('obj2',obj2);

通過(guò)上面這段代碼可以看到執(zhí)行結(jié)果如下圖所示。


運(yùn)行示例

再來(lái)一個(gè)示例

const newState = Object.assign({}, state)和JSON.parse(JSON.stringify(obj))都是可以用來(lái)深拷貝 但是也會(huì)出現(xiàn)下面的問(wèn)題

  1. 如果obj里面存在時(shí)間對(duì)象,JSON.parse(JSON.stringify(obj))之后锉桑,時(shí)間對(duì)象變成了字符串排霉。
  2. 如果obj里有RegExp、Error對(duì)象民轴,則序列化的結(jié)果將只得到空對(duì)象攻柠。
  3. 如果obj里有函數(shù),undefined后裸,則序列化的結(jié)果會(huì)把函數(shù)瑰钮, undefined丟失。
  4. 如果obj里有NaN微驶、Infinity和-Infinity浪谴,則序列化的結(jié)果會(huì)變成null。
  5. JSON.stringify()只能序列化對(duì)象的可枚舉的自有屬性。如果obj中的對(duì)象是有構(gòu)造函數(shù)生成的苟耻,
    則使用JSON.parse(JSON.stringify(obj))深拷貝后篇恒,會(huì)丟棄對(duì)象的constructor。
    如果對(duì)象中存在循環(huán)引用的情況也無(wú)法正確實(shí)現(xiàn)深拷貝梁呈。
function Person (name) {
    this.name = 20
}
const lili = new Person('lili')
let a = {
    data0: '1',
    date1: [new Date('2020-03-01'), new Date('2020-03-05')],
    data2: new RegExp('\\w+'),
    data3: new Error('1'),
    data4: undefined,
    data5: function () {
        console.log(1)
    },
    data6: NaN,
    data7: lili
}
// 合并對(duì)象  
//   const newState = Object.assign({}, state)
// 先石化  再解封  就重新開(kāi)辟空間了   
let b = JSON.parse(JSON.stringify(a))
console.log(b);
執(zhí)行結(jié)果

使用 JSON.stringify 方法實(shí)現(xiàn)深拷貝對(duì)象婚度,雖然到目前為止還有很多無(wú)法實(shí)現(xiàn)的功能,但是這種方法足以滿足日常的開(kāi)發(fā)需求官卡,并且是最簡(jiǎn)單和快捷的蝗茁。而對(duì)于其他的也要實(shí)現(xiàn)深拷貝的,比較麻煩的屬性對(duì)應(yīng)的數(shù)據(jù)類型寻咒,JSON.stringify 暫時(shí)還是無(wú)法滿足的哮翘,那么就需要下面的幾種方法了。

方法二:基礎(chǔ)版(手寫遞歸實(shí)現(xiàn))

下面是一個(gè)實(shí)現(xiàn) deepClone 函數(shù)封裝的例子毛秘,通過(guò) for in 遍歷傳入?yún)?shù)的屬性值饭寺,如果值是引用類型則再次遞歸調(diào)用該函數(shù),如果是基礎(chǔ)數(shù)據(jù)類型就直接復(fù)制叫挟,代碼如下所示艰匙。

let obj1 = {
  a:{
    b:1
  }
}

function deepClone(obj) { 
  let cloneObj = {}
  for(let key in obj) {                 //遍歷
    if(typeof obj[key] ==='object') { 
      cloneObj[key] = deepClone(obj[key])  //是對(duì)象就再次調(diào)用該函數(shù)遞歸
    } else {
      cloneObj[key] = obj[key]  //基本類型的話直接復(fù)制值
    }

  }
  return cloneObj

}

let obj2 = deepClone(obj1);
obj1.a.b = 2;
console.log(obj2);   //  {a:{b:1}}

雖然利用遞歸能實(shí)現(xiàn)一個(gè)深拷貝,但是同上面的 JSON.stringify 一樣抹恳,還是有一些問(wèn)題沒(méi)有完全解決员凝,例如:

  1. 這個(gè)深拷貝函數(shù)并不能復(fù)制不可枚舉的屬性以及 Symbol 類型;
  2. 這種方法只是針對(duì)普通的引用類型的值做遞歸復(fù)制奋献,而對(duì)于 Array健霹、Date、RegExp瓶蚂、Error糖埋、Function 這樣的引用類型并不能正確地拷貝;
  3. 對(duì)象的屬性里面成環(huán)窃这,即循環(huán)引用沒(méi)有解決瞳别。

這種基礎(chǔ)版本的寫法也比較簡(jiǎn)單,可以應(yīng)對(duì)大部分的應(yīng)用情況杭攻。但是你在面試的過(guò)程中洒试,如果只能寫出這樣的一個(gè)有缺陷的深拷貝方法,有可能不會(huì)通過(guò)朴上。

所以為了“拯救”這些缺陷,下面我?guī)阋黄鹂纯锤倪M(jìn)的版本卒煞,以便于你可以在面試種呈現(xiàn)出更好的深拷貝方法痪宰,贏得面試官的青睞。

方法三:改進(jìn)版(改進(jìn)后遞歸實(shí)現(xiàn))

針對(duì)上面幾個(gè)待解決問(wèn)題,我先通過(guò)四點(diǎn)相關(guān)的理論告訴你分別應(yīng)該怎么做衣撬。

  1. 針對(duì)能夠遍歷對(duì)象的不可枚舉屬性以及 Symbol 類型乖订,我們可以使用 Reflect.ownKeys 方法;
  2. 當(dāng)參數(shù)為 Date具练、RegExp 類型乍构,則直接生成一個(gè)新的實(shí)例返回;
  3. 利用 Object 的 getOwnPropertyDescriptors 方法可以獲得對(duì)象的所有屬性扛点,以及對(duì)應(yīng)的特性哥遮,順便結(jié)合 Object 的 create 方法創(chuàng)建一個(gè)新對(duì)象,并繼承傳入原對(duì)象的原型鏈陵究;
  4. 利用 WeakMap 類型作為 Hash 表眠饮,因?yàn)?WeakMap 是弱引用類型,可以有效防止內(nèi)存泄漏(你可以關(guān)注一下 Map 和 weakMap 的關(guān)鍵區(qū)別铜邮,這里要用 weakMap)仪召,作為檢測(cè)循環(huán)引用很有幫助,如果存在循環(huán)松蒜,則引用直接返回 WeakMap 存儲(chǔ)的值扔茅。

關(guān)于第 4 點(diǎn)的 WeakMap,這里我不進(jìn)行過(guò)多的科普講解了秸苗,你如果不清楚可以自己再通過(guò)相關(guān)資料了解一下召娜。我也經(jīng)常在給人面試中看到有人使用 WeakMap 來(lái)解決循環(huán)引用問(wèn)題,但是很多解釋都是不夠清晰的难述。

當(dāng)你不太了解 WeakMap 的真正作用時(shí)萤晴,我建議你不要在面試中寫出這樣的代碼,如果只是死記硬背胁后,會(huì)給自己挖坑的店读。因?yàn)槟銓懙拿恳恍写a都是需要經(jīng)過(guò)深思熟慮并且非常清晰明白的,這樣你才能經(jīng)得住面試官的推敲攀芯。

當(dāng)然屯断,如果你在考慮到循環(huán)引用的問(wèn)題之后,還能用 WeakMap 來(lái)很好地解決侣诺,并且向面試官解釋這樣做的目的殖演,那么你所展示的代碼,以及你對(duì)問(wèn)題思考的全面性年鸳,在面試官眼中應(yīng)該算是合格的了趴久。

那么針對(duì)上面這幾個(gè)問(wèn)題,我們來(lái)看下改進(jìn)后的遞歸實(shí)現(xiàn)的深拷貝代碼應(yīng)該是什么樣子的搔确,如下所示彼棍。

const isComplexDataType = obj => (typeof obj === 'object' || typeof obj === 'function') && (obj !== null)
const deepClone = function (obj, hash = new WeakMap()) {
  if (obj.constructor === Date) 
  return new Date(obj)       // 日期對(duì)象直接返回一個(gè)新的日期對(duì)象
  if (obj.constructor === RegExp)
  return new RegExp(obj)     //正則對(duì)象直接返回一個(gè)新的正則對(duì)象
  //如果循環(huán)引用了就用 weakMap 來(lái)解決
  if (hash.has(obj)) return hash.get(obj)
  let allDesc = Object.getOwnPropertyDescriptors(obj)
  //遍歷傳入?yún)?shù)所有鍵的特性
  let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc)
  //繼承原型鏈
  hash.set(obj, cloneObj)
  for (let key of Reflect.ownKeys(obj)) { 
    cloneObj[key] = (isComplexDataType(obj[key]) && typeof obj[key] !== 'function') ? deepClone(obj[key], hash) : obj[key]
  }
  return cloneObj
}

// 下面是驗(yàn)證代碼

let obj = {
  num: 0,
  str: '',
  boolean: true,
  unf: undefined,
  nul: null,
  obj: { name: '我是一個(gè)對(duì)象', id: 1 },
  arr: [0, 1, 2],
  func: function () { console.log('我是一個(gè)函數(shù)') },
  date: new Date(0),
  reg: new RegExp('/我是一個(gè)正則/ig'),
  [Symbol('1')]: 1,
};

Object.defineProperty(obj, 'innumerable', {
  enumerable: false, value: '不可枚舉屬性' }
);
obj = Object.create(obj, Object.getOwnPropertyDescriptors(obj))
obj.loop = obj    // 設(shè)置loop成循環(huán)引用的屬性
let cloneObj = deepClone(obj)
cloneObj.arr.push(4)
console.log('obj', obj)
console.log('cloneObj', cloneObj)

我們看一下結(jié)果灭忠,cloneObj 在 obj 的基礎(chǔ)上進(jìn)行了一次深拷貝,cloneObj 里的 arr 數(shù)組進(jìn)行了修改座硕,并未影響到 obj.arr 的變化弛作,如下圖所示。

示例運(yùn)行

從這張截圖的結(jié)果可以看出华匾,改進(jìn)版的 deepClone 函數(shù)已經(jīng)對(duì)基礎(chǔ)版的那幾個(gè)問(wèn)題進(jìn)行了改進(jìn)映琳。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市蜘拉,隨后出現(xiàn)的幾起案子萨西,更是在濱河造成了極大的恐慌,老刑警劉巖诸尽,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件原杂,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡您机,警方通過(guò)查閱死者的電腦和手機(jī)穿肄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)际看,“玉大人咸产,你說(shuō)我怎么就攤上這事≈倜觯” “怎么了脑溢?”我有些...
    開(kāi)封第一講書人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)赖欣。 經(jīng)常有香客問(wèn)我屑彻,道長(zhǎng),這世上最難降的妖魔是什么顶吮? 我笑而不...
    開(kāi)封第一講書人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任社牲,我火速辦了婚禮,結(jié)果婚禮上悴了,老公的妹妹穿的比我還像新娘搏恤。我一直安慰自己,他們只是感情好湃交,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布熟空。 她就那樣靜靜地躺著,像睡著了一般搞莺。 火紅的嫁衣襯著肌膚如雪息罗。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 51,292評(píng)論 1 301
  • 那天才沧,我揣著相機(jī)與錄音阱当,去河邊找鬼俏扩。 笑死,一個(gè)胖子當(dāng)著我的面吹牛弊添,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播捌木,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼油坝,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了刨裆?” 一聲冷哼從身側(cè)響起澈圈,我...
    開(kāi)封第一講書人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎帆啃,沒(méi)想到半個(gè)月后瞬女,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡努潘,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年诽偷,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片疯坤。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡报慕,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出压怠,到底是詐尸還是另有隱情眠冈,我是刑警寧澤,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布菌瘫,位于F島的核電站蜗顽,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏雨让。R本人自食惡果不足惜雇盖,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望宫患。 院中可真熱鬧刊懈,春花似錦、人聲如沸娃闲。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)皇帮。三九已至卷哩,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間属拾,已是汗流浹背将谊。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工冷溶, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人尊浓。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓逞频,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親栋齿。 傳聞我的和親對(duì)象是個(gè)殘疾皇子苗胀,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容