深拷貝的原理和實(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é)果如下圖所示。
再來(lái)一個(gè)示例
const newState = Object.assign({}, state)和JSON.parse(JSON.stringify(obj))都是可以用來(lái)深拷貝 但是也會(huì)出現(xiàn)下面的問(wèn)題
- 如果obj里面存在時(shí)間對(duì)象,JSON.parse(JSON.stringify(obj))之后锉桑,時(shí)間對(duì)象變成了字符串排霉。
- 如果obj里有RegExp、Error對(duì)象民轴,則序列化的結(jié)果將只得到空對(duì)象攻柠。
- 如果obj里有函數(shù),undefined后裸,則序列化的結(jié)果會(huì)把函數(shù)瑰钮, undefined丟失。
- 如果obj里有NaN微驶、Infinity和-Infinity浪谴,則序列化的結(jié)果會(huì)變成null。
- 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);
使用 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)有完全解決员凝,例如:
- 這個(gè)深拷貝函數(shù)并不能復(fù)制不可枚舉的屬性以及 Symbol 類型;
- 這種方法只是針對(duì)普通的引用類型的值做遞歸復(fù)制奋献,而對(duì)于 Array健霹、Date、RegExp瓶蚂、Error糖埋、Function 這樣的引用類型并不能正確地拷貝;
- 對(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)該怎么做衣撬。
- 針對(duì)能夠遍歷對(duì)象的不可枚舉屬性以及 Symbol 類型乖订,我們可以使用 Reflect.ownKeys 方法;
- 當(dāng)參數(shù)為 Date具练、RegExp 類型乍构,則直接生成一個(gè)新的實(shí)例返回;
- 利用 Object 的 getOwnPropertyDescriptors 方法可以獲得對(duì)象的所有屬性扛点,以及對(duì)應(yīng)的特性哥遮,順便結(jié)合 Object 的 create 方法創(chuàng)建一個(gè)新對(duì)象,并繼承傳入原對(duì)象的原型鏈陵究;
- 利用 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 的變化弛作,如下圖所示。
從這張截圖的結(jié)果可以看出华匾,改進(jìn)版的 deepClone 函數(shù)已經(jīng)對(duì)基礎(chǔ)版的那幾個(gè)問(wèn)題進(jìn)行了改進(jìn)映琳。