手寫實(shí)現(xiàn)深度拷貝
基礎(chǔ)理論
拷貝的基礎(chǔ)是賦值,在 js 中手趣,將一個(gè)變量賦值給另一個(gè)變量時(shí),有兩種場(chǎng)景:
- 基本類型數(shù)據(jù)的值拷貝
- 引用類型數(shù)據(jù)的引用拷貝
var a = 1;
var b = {a: 1};
var a1 = a;
var b1 = b;
var b2 = b;
a1 = 2;
a; // 1 原始類型的賦值是值拷貝骑祟,兩不影響
b1 = null;
b; // {a: 1} 對(duì)象類型的賦值是引用拷貝,修改引用指向气笙,對(duì)原變量無(wú)影響
b2.a = 2;
b; // {a: 2} 對(duì)象類型的賦值是引用拷貝次企,指向同一份對(duì)象,修改對(duì)象屬性潜圃,會(huì)對(duì)原變量指向的對(duì)象有所影響
那么缸棵,對(duì)一個(gè)對(duì)象進(jìn)行拷貝,無(wú)非就是對(duì)對(duì)象的屬性進(jìn)行拷貝谭期,按照拷貝處理的方式不同堵第,可分為淺拷貝和深拷貝:
- 淺拷貝是只拷貝對(duì)象的第一層屬性
- 深拷貝則是無(wú)限層次的拷貝對(duì)象的屬性,只要屬性值不是基本類型隧出,就繼續(xù)深度遍歷進(jìn)去
淺拷貝的雙方仍舊有所關(guān)聯(lián)踏志,因?yàn)橛行傩灾皇且每截惗眩际侵赶蛲环輸?shù)據(jù)胀瞪,一方修改就會(huì)影響到另一方针余;
深拷貝的雙方則是相互獨(dú)立,互不影響凄诞。
在 js 中圆雁,內(nèi)置的各種復(fù)制、拷貝的 API帆谍,都是淺拷貝伪朽,比如:Object.assign(),{...a}汛蝙,[].slice() 等等烈涮。
如果項(xiàng)目中有需要使用到深拷貝,那么就只能是自行實(shí)現(xiàn)窖剑,或者使用三方庫(kù)跃脊。
實(shí)現(xiàn)深拷貝
有人可能會(huì)覺得自己實(shí)現(xiàn)個(gè)深拷貝很簡(jiǎn)單,畢竟都已經(jīng)知道淺拷貝只拷貝一層苛吱,那深拷貝不就等效于淺拷貝 + 遞歸酪术?
function cloneDeep(source) {
let target = {};
for (let key in source) {
if (typeof source[key] === 'object') {
target[key] = cloneDeep(source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
那么,上面的深拷貝實(shí)現(xiàn)有問(wèn)題嗎?
雖然從概念上绘雁,深拷貝就是需要層層遍歷對(duì)象屬性橡疼,只拷貝基本類型數(shù)據(jù),對(duì)象類型再繼續(xù)深入遍歷庐舟,反應(yīng)到代碼上欣除,的確也就是像上面的處理:基本類型值拷貝 + 對(duì)象類型遞歸處理。
但上例的代碼挪略,欠缺各種細(xì)節(jié)和場(chǎng)景的處理历帚。
比如說(shuō):
- 參數(shù) source 的校驗(yàn)
- typeof null 也是 object 的過(guò)濾處理
- 屬性 key 值類型是 Symbol 的場(chǎng)景
- source 是數(shù)組時(shí)的兼容處理
- 循環(huán)引用的場(chǎng)景
- 引用關(guān)系丟失問(wèn)題
- 遞歸棧溢出的場(chǎng)景
- 等等
所以,本篇想講的深拷貝實(shí)現(xiàn)杠娱,就是希望能把這些細(xì)節(jié)和特殊場(chǎng)景考慮進(jìn)去挽牢,同時(shí),也會(huì)介紹一些不同的實(shí)現(xiàn)方案摊求。
通用版
想要實(shí)現(xiàn)通用版禽拔,其實(shí)也就是要將上面列出來(lái)的細(xì)節(jié)和各自場(chǎng)景考慮進(jìn)行,思考每個(gè)問(wèn)題該如何解決:
- 參數(shù) source 的校驗(yàn) & null 的過(guò)濾處理
畢竟如果不是對(duì)象的話室叉,也就沒有什么拷貝的意義了睹栖,直接原值返回即可,所以這里需要對(duì)參數(shù)進(jìn)行是否是對(duì)象的判斷處理茧痕。
使用 typeof 的話野来,由于 null 也是 object,所以還需要將 null 的場(chǎng)景過(guò)濾掉踪旷;
function isObject(o) {
return typeof o === 'object' && o !== null;
}
- Symbol 的處理
symbol 是 ES6 中新增的特性梁只,使用 for in 方式遍歷不到,所以需要 ES6 中新增的遍歷方式:
- Object.getOwnPropertySymbols()
- Reflect.ownKeys()
前者是單獨(dú)遍歷對(duì)象鍵值類型為 Symbol 的屬性埃脏,使用這種方式的話搪锣,等于是分兩次處理對(duì)象,先深拷貝一次 Symbol 屬性彩掐,再深拷貝一次其他屬性构舟。
后者 Reflect.ownKeys() 可以遍歷到對(duì)象所有的自有屬性,包括 Symbol 屬性堵幽,它相當(dāng)于 Object.getOwnPropertyNames() 和 Object.getOwnPropertySymbols() 的并集狗超。使用這種方式的話,等于替換掉 for in 的遍歷方式朴下。
- 數(shù)組的兼容處理
這個(gè)的意思是說(shuō)努咐,需要區(qū)分當(dāng)前拷貝的對(duì)象是數(shù)組還是對(duì)象,畢竟總不能將數(shù)組的數(shù)據(jù)拷貝到對(duì)象里把殴胧,所以 target 的初始化需要處理一下渗稍,區(qū)分?jǐn)?shù)組的場(chǎng)景:
let target = Array.isArray(source) ? [] : {};
- 循環(huán)引用 & 引用關(guān)系丟失問(wèn)題
這種場(chǎng)景還是用代碼來(lái)解釋比較清晰:
var a = {};
var o = {
a: a,
b: a
}
o.c = o;
o; // {a: {}, b: {}, c: {a: {}, b: {}, c:{...}}}
o.a === o.b; // true
var o1 = cloneDeep(o); // 棧溢出異常佩迟,Maximum call stack size
// 把 o.c = o 注釋掉, o1.a === o1.b 輸出 false竿屹,原本的引用關(guān)系丟失了
循環(huán)引用指的是报强,對(duì)象的某個(gè)屬性又指向了對(duì)象本身,這樣就造成了具有無(wú)限深層次的結(jié)構(gòu)拱燃,遞歸時(shí)自然就會(huì)棧溢出了秉溉。
引用關(guān)系丟失指的是,對(duì)象的多個(gè)屬性都指向同一個(gè)某對(duì)象碗誉,但經(jīng)過(guò)深拷貝后召嘶,這多個(gè)屬性卻都指向了不同的對(duì)象,雖然被指向的這些對(duì)象的值是一致的哮缺。
造成這兩個(gè)問(wèn)題的根本原因弄跌,其實(shí)就是,對(duì)于對(duì)象數(shù)據(jù)蝴蜓,每次都會(huì)重新創(chuàng)建一個(gè)新對(duì)象來(lái)存儲(chǔ)拷貝過(guò)來(lái)的值碟绑。
所以俺猿,解決這兩個(gè)問(wèn)題茎匠,其實(shí)也很簡(jiǎn)單,就是不要每次都重新創(chuàng)建新的對(duì)象押袍,復(fù)用已創(chuàng)建的對(duì)象即可诵冒。
比如說(shuō),在遍歷拷貝 o.a 時(shí)谊惭,先創(chuàng)建一個(gè)新對(duì)象拷貝了 o.a汽馋,之后再處理 o.b 時(shí),發(fā)現(xiàn) o.b 也指向 o.a圈盔,那么就不要重新創(chuàng)建對(duì)象來(lái)拷貝了豹芯,直接將引用指向之前拷貝 o.a 時(shí)創(chuàng)建的對(duì)象即可,這樣引用關(guān)系就保留下來(lái)了驱敲。
這樣即使遇到循環(huán)引用铁蹈,就將引用指向拷貝生成的新對(duì)象即可,就不會(huì)有棧溢出的場(chǎng)景了众眨。
代碼上的話握牧,可以利用 ES6 的 map 數(shù)據(jù)結(jié)構(gòu),因?yàn)榭梢灾苯幼?source 對(duì)象作為 key 來(lái)存儲(chǔ)娩梨。
否則就得自己用數(shù)組存儲(chǔ)沿腰,但由于數(shù)組 key 值也只能是字符串和 Symbol,所以映射關(guān)系只能自己用對(duì)象存狈定,這么一來(lái)颂龙,還得自己寫尋找的邏輯。
function cloneDeep(source, hash = new WeakMap()) {
// ... 省略
if (hash.get(source)) {
return hash.get(source)
}
let target = Array.isArray(source) ? [] : {};
hash.set(source, target);
// target[key] = cloneDeep(source[key], hash); // 對(duì)象類型遞歸調(diào)用時(shí),將 hash 傳遞進(jìn)去
// .., 省略
}
function cloneDeep(source, hash = []) {
// ... 省略
let cache = hash.find(v => v.source === source);
if (cache) {
return cache.target;
}
let target = Array.isArray(source) ? [] : {};
hash.push({ source: source, target: target });
// target[key] = cloneDeep(source[key], hash); // 對(duì)象類型遞歸調(diào)用時(shí)厘托,將 hash 傳遞進(jìn)去
// ... 省略
}
- 棧溢出問(wèn)題
遞歸的最大問(wèn)題友雳,就是怕遇到棧溢出,一旦遞歸層次多的話铅匹。
循環(huán)引用會(huì)導(dǎo)致遞歸層次過(guò)多而棧溢出押赊,但可以通過(guò)已拷貝對(duì)象的緩存來(lái)解決這個(gè)問(wèn)題。
但如果對(duì)象的結(jié)構(gòu)層次過(guò)多時(shí)包斑,這種現(xiàn)象就無(wú)法避免了流礁,就必須來(lái)解決棧溢出問(wèn)題了。
解決棧溢出兩種思路:
- 尾遞歸優(yōu)化
- 不用遞歸罗丰,改成循環(huán)實(shí)現(xiàn)
尾遞歸優(yōu)化是指函數(shù)的最后一行代碼都是調(diào)用自身函數(shù)神帅,如果可以修改成這種模式,就可以達(dá)到尾遞歸優(yōu)化萌抵。而這種方式之所以可以解決棧溢出找御,是因?yàn)椋瘮?shù)的最后一行都是調(diào)用自身函數(shù)绍填,那其實(shí)就意味著當(dāng)前函數(shù)執(zhí)行上下文其實(shí)沒必要保留了霎桅,之所以會(huì)棧溢出,就是執(zhí)行上下文棧中存在過(guò)多函數(shù)執(zhí)行上下文讨永。
每次調(diào)用函數(shù)都會(huì)創(chuàng)建一個(gè)函數(shù)執(zhí)行上下文(EC)滔驶,并放入執(zhí)行上下文棧(ECS)中,當(dāng)函數(shù)執(zhí)行結(jié)束時(shí)卿闹,就將函數(shù)執(zhí)行上下文移出棧揭糕。
所以,函數(shù)內(nèi)部嵌套調(diào)用函數(shù)時(shí)锻霎,就會(huì)造成 ECS 中有過(guò)多的 EC著角,遞歸是不斷的在函數(shù)內(nèi)調(diào)用自己,所以一旦層次過(guò)多旋恼,必然導(dǎo)致 ECS 爆表吏口,棧溢出。
而尾遞歸蚌铜,讓遞歸函數(shù)的最后一行執(zhí)行的代碼都是調(diào)用自身锨侯,這就意味著,在遞歸調(diào)用自身時(shí)冬殃,當(dāng)前函數(shù)的職責(zé)已結(jié)束囚痴,那么 EC 其實(shí)就可以從 ECS 中移出了,這樣一來(lái)审葬,不管遞歸層次多深深滚,始終都只有一個(gè)遞歸函數(shù)的 EC 在 ECS 中奕谭,自然就不會(huì)造成棧溢出。
不過(guò)尾遞歸優(yōu)化有個(gè)局限性痴荐,只在嚴(yán)格模式下才開啟血柳,因?yàn)榉菄?yán)格模式下,函數(shù)內(nèi)部有 arguments 和 caller 兩個(gè)變量會(huì)追蹤調(diào)用棧生兆,尾遞歸優(yōu)化會(huì)導(dǎo)致這兩變量失真報(bào)錯(cuò)难捌,所以只在嚴(yán)格模式下才開啟。
而且鸦难,正常遞歸函數(shù)改寫成尾遞歸根吁,基本操作都是將局部變量變成參數(shù),保證最后執(zhí)行的一行代碼是調(diào)用自身合蔽。但由于深拷貝場(chǎng)景击敌,是在遍歷屬性過(guò)程中遞歸調(diào)用自身,調(diào)用完自身后面肯定還需要遍歷處理其他屬性拴事,所以無(wú)法做到最后一行調(diào)用自身的要求沃斤,也就無(wú)法改寫成尾遞歸形式。
所以刃宵,尾遞歸優(yōu)化這種方案放棄衡瓶。
用循環(huán)替代遞歸是另外一種解決棧溢出方案,這種方式其實(shí)就是思考组去,原本需要使用遞歸的方式鞍陨,有沒有辦法通過(guò)循環(huán)來(lái)實(shí)現(xiàn)步淹。循環(huán)的話从隆,也就不存在什么嵌套調(diào)用函數(shù),也就不存在棧溢出的問(wèn)題了缭裆。
對(duì)象的屬性結(jié)構(gòu)键闺,其實(shí)就是一顆樹結(jié)構(gòu),遞歸方案的深拷貝澈驼,其實(shí)也就是以深度優(yōu)先來(lái)遍歷對(duì)象的屬性樹辛燥。
但遍歷樹結(jié)構(gòu)數(shù)據(jù),除了使用遞歸方案外缝其,也可以使用循環(huán)來(lái)遍歷挎塌,但是需要借助相應(yīng)的數(shù)據(jù)結(jié)構(gòu)。
當(dāng)使用循環(huán)來(lái)遍歷樹内边,且深度優(yōu)先時(shí)榴都,那么就需要借助棧;如果是廣度優(yōu)先時(shí)漠其,則是需要借助隊(duì)列嘴高。
具體做法則是竿音,一次只處理一個(gè)節(jié)點(diǎn),處理節(jié)點(diǎn)時(shí)遍歷取出它所有子節(jié)點(diǎn)拴驮,代碼上也就是雙層循環(huán)春瞬,比如說(shuō):
- 從樹根節(jié)點(diǎn)開始,遍歷它的第一層子節(jié)點(diǎn)套啤,把這些節(jié)點(diǎn)都放入椏砥或隊(duì)列中,結(jié)束本次循環(huán)潜沦;
- 下次循環(huán)開始抹竹,取出棧頂或隊(duì)頭節(jié)點(diǎn)處理:若該節(jié)點(diǎn)還有子節(jié)點(diǎn),那么遍歷取出所有子節(jié)點(diǎn)止潮,放入椙耘校或隊(duì)列中,結(jié)束本次循環(huán)喇闸;
- 重復(fù)第2步袄琳,直至棧或隊(duì)列中無(wú)節(jié)點(diǎn)燃乍;
- 如果是用棧輔助唆樊,則對(duì)應(yīng)深度優(yōu)先遍歷;如果是用隊(duì)列輔助刻蟹,則對(duì)應(yīng)廣度優(yōu)先逗旁。
所以,這里用循環(huán)遍歷對(duì)象屬性樹的方式來(lái)解決棧溢出問(wèn)題舆瘪。
- 代碼
最后就看看實(shí)現(xiàn)的代碼片效,這里給出兩個(gè)版本,分別是未處理?xiàng)R绯鰣?chǎng)景(遞歸方案)英古、循環(huán)替代遞歸:
未處理?xiàng)R绯霭妫ㄟf歸方案):
// 遞歸遍歷對(duì)象的屬性樹
function cloneDeep(source, hash = new WeakMap()) {
// 1. 非對(duì)象類型數(shù)據(jù)淀衣,直接返回
if (!(typeof source === 'object' && source !== null)) {
return source;
}
// 2. 復(fù)用已拷貝的對(duì)象,解決引用關(guān)系丟失和循環(huán)引用問(wèn)題
if (hash.get(source)) {
return hash.get(source);
}
// 3. 區(qū)分對(duì)象和數(shù)組
let target = Array.isArray(source) ? [] : {};
hash.set(source, target); // 緩存已拷貝的對(duì)象
// 4. 遍歷對(duì)象所有自有屬性召调,包括 Symbol
Reflect.ownKeys(source).forEach(key => {
// 跳過(guò)自有的不可枚舉的屬性
if (!Object.getOwnPropertyDescriptor(source, key).enumerable) {
return;
}
// 對(duì)象類型再繼續(xù)遞歸遍歷膨桥,其他類型直接賦值拷貝
if (typeof source === 'object' && source !== null) {
target[key] = cloneDeep(source[key], hash);
} else {
target[key] = source[key];
}
});
return target;
}
循環(huán)替代遞歸版(循環(huán)方案):
// 循環(huán)遍歷對(duì)象的屬性樹,跟遞歸方案中相同代碼用途是一樣的唠叛,這里就不注釋了
function cloneDeep(source) {
if (!(typeof source === 'object' && source !== null)) {
return source;
}
let target = Array.isArray(source) ? [] : {};
let hash = new WeakMap();
// 將根節(jié)點(diǎn)放入棧中只嚣,節(jié)點(diǎn)結(jié)構(gòu)說(shuō)明:data 存儲(chǔ)當(dāng)前屬性值,key 存儲(chǔ)屬性名艺沼,target 含義:target[key] = data
let stack = [{
data: source,
key: undefined,
target: target
}];
// 因?yàn)槭墙柚?stack 棧輔助册舞,所以是深度優(yōu)先遍歷,每次循環(huán)只處理一個(gè)節(jié)點(diǎn)
while(stack.length > 0) {
let node = stack.pop();
if (typeof node.data === 'object' && node.data !== null) {
// 當(dāng)前對(duì)象有已拷貝過(guò)的緩存澳厢,則直接用緩存环础,解決引用關(guān)系丟失問(wèn)題
if (hash.get(node.data)) {
node.target[node.key] = hash.get(node.data);
} else {
let target;
// 構(gòu)建拷貝對(duì)象的屬性層次結(jié)構(gòu)
if (node.key !== undefined) {
target = Array.isArray(node.data) ? [] : {};
node.target[node.key] = target;
} else {
target = node.target;
}
hash.set(node.data, target);
Reflect.ownKeys(node.data).forEach(v =>{
if (!Object.getOwnPropertyDescriptor(node.data, v).enumerable) {
return;
}
stack.push({
data: node.data[v],
key: v,
target: target
})
});
}
} else {
// 當(dāng)前節(jié)點(diǎn)是非對(duì)象類型囚似,直接拷貝
node.target[node.key] = node.data;
}
}
return target;
}
測(cè)試用例:
// 測(cè)試用例
var a = {};
var o = {
a: a,
b: a,
c: Symbol(),
[Symbol()]: 1,
d: function() {},
e(){},
f: () => {},
get g(){},
h: 1,
i: 'sdff',
j: null,
k: undefined,
o: /sdfdf/,
p: new Date()
}
var o1 = cloneDeep(o);
o1;
/**
{
a: {}
b: {}
c: Symbol()
d: ? ()
e: ? e()
f: () => {}
g: undefined
h: 1
i: "sdff"
j: null
k: undefined
l: {l: {…}, p: {…}, o: {…}, k: undefined, j: null, …}
o: {}
p: {}
Symbol(): 1
}
*/
// 正則的數(shù)據(jù)和 Date 數(shù)據(jù)都丟失了,這是因?yàn)榕袛鄬?duì)象的邏輯導(dǎo)致线得,typeof xx === 'object' 無(wú)法區(qū)別內(nèi)置對(duì)象饶唤,想要解決,可以修改判斷對(duì)象的邏輯贯钩,比如使用 Object.prototype.toString.call(xxx) 結(jié)合 Array.isArray 來(lái)只篩選出基本對(duì)象和數(shù)組類型
// get 存取器也只能拷貝到讀取的時(shí)募狂,無(wú)法拷貝 get 方法
// 測(cè)試棧溢出場(chǎng)景可借助該方法
function createData(deep, breadth) {
var data = {};
var temp = data;
for (var i = 0; i < deep; i++) {
temp = temp['data'] = {};
for (var j = 0; j < breadth; j++) {
temp[j] = j;
}
}
return data;
}
var a = createData(10000);
cloneDeep(a); // 是否會(huì)棧溢出
其實(shí),這通用版也不是100%通用角雷,它仍舊有一些局限性祸穷,比如:
- 沒有考慮 ES6 的 set,Map 等新的數(shù)據(jù)結(jié)構(gòu)類型
- 拷貝后的對(duì)象的原型鏈結(jié)構(gòu)勺三,繼承關(guān)系丟失問(wèn)題
- get雷滚,set 存取器邏輯無(wú)法拷貝
- 沒有考慮屬性值是內(nèi)置對(duì)象的場(chǎng)景,比如 /sfds/ 正則吗坚,或 new Date() 日期這些類型的數(shù)據(jù)
- 等等
雖然如此祈远,但這種方案已經(jīng)大體上適用于絕大多數(shù)的場(chǎng)景了,如有問(wèn)題商源,或者有新的需求车份,再根據(jù)需要進(jìn)行擴(kuò)展就可以了,歡迎指點(diǎn)一下牡彻。
JSON.parse/stringify 版
這是實(shí)現(xiàn)深拷貝最簡(jiǎn)單的一種方案扫沼,但是有很大局限性,只適用于部分場(chǎng)景:
var o = {
a: 1,
b: [1, 2, {a: 1}]
}
var o1 = JSON.parse(JSON.stringify(o));
它的原理很簡(jiǎn)單庄吼,就是借助現(xiàn)有工具 JSON缎除,先將對(duì)象序列化,再反序列化得到一個(gè)新對(duì)象霸褒,這樣一來(lái)伴找,新對(duì)象跟原對(duì)象就是兩個(gè)相互獨(dú)立盈蛮,互不影響的對(duì)象了废菱,以此來(lái)實(shí)現(xiàn)深拷貝。
但它有很大的局限性抖誉,因?yàn)樾枰蕾囉?JSON 的序列化和反序列化基礎(chǔ)殊轴,比如說(shuō):
- 不能序列化函數(shù),屬性值是函數(shù)的會(huì)丟失掉
- 不能處理 Symbol 數(shù)據(jù)袒炉,不管是屬性名還是屬性值是 Symbol 的旁理,都會(huì)丟失掉
- 不能識(shí)別屬性值手動(dòng)設(shè)置為 undefined 的場(chǎng)景,會(huì)被認(rèn)為是訪問(wèn)一個(gè)不存在的屬性我磁,從而導(dǎo)致丟失
- 不能解決循環(huán)引用問(wèn)題
- 不能處理正則
- 等等
使用這種方案孽文,還是有很多局限性驻襟,看個(gè)代碼就清楚了:
var o = {
a: 1,
[Symbol()]: 1,
c: Symbol(),
d: null,
e: undefined,
f: function() {},
g() {},
h: /sdfd/
}
var o1 = JSON.parse(JSON.stringify(o));
o1; // {a: 1, d: null, h: {}}
// 屬性 c, e, f, g 都丟失掉了,h 屬性值為正則表達(dá)式芋哭,也無(wú)法正常處理
那么沉衣,這種方案的深拷貝就沒有什么用處嗎?
也不是减牺,它有它適用的場(chǎng)景豌习,想想 JSON 是什么,是處理 json 對(duì)象的工具啊拔疚,而 json 對(duì)象通常是用來(lái)跟服務(wù)端交互的數(shù)據(jù)結(jié)構(gòu)肥隆,在這種場(chǎng)景里,你一個(gè) json 對(duì)象里稚失,會(huì)有那些 Symbol栋艳、正則、函數(shù)奇奇怪怪的屬性嗎句各?顯然不會(huì)嘱巾。
所以,對(duì)于規(guī)范的 json 對(duì)象來(lái)說(shuō)诫钓,如果需要進(jìn)行深拷貝旬昭,那么就可以使用這種方案。
通俗點(diǎn)說(shuō)菌湃,在項(xiàng)目中的使用場(chǎng)景也就是對(duì)后端接口返回的 json 數(shù)據(jù)需要深拷貝時(shí)问拘,就可以使用這種方案。
(以上個(gè)人理解惧所,有誤的話骤坐,歡迎指點(diǎn)一下)
Object.assign 版
上面的深拷貝方案只是將一個(gè)對(duì)象完完整整拷貝一份出來(lái),新對(duì)象數(shù)據(jù)和原對(duì)象數(shù)據(jù)都是一模一樣的下愈,算是副本纽绍。
但如果,需求是要類似 Object.assign 這種势似,將一個(gè)原對(duì)象完完整整拷貝到另一個(gè)已存在的目標(biāo)對(duì)象上面呢拌夏?這種場(chǎng)景,拷貝后的新對(duì)象就跟原對(duì)象不是一樣的了履因,而是兩者的交集障簿,沖突的拷貝的原對(duì)象為主。
這種場(chǎng)景上面的深拷貝方案就不適用了栅迄,這里參考 Object.assign 原理擴(kuò)展實(shí)現(xiàn) assignDeep站故,實(shí)現(xiàn)可將指定的原對(duì)象們,拷貝到已存在的目標(biāo)對(duì)象上:
// 遞歸版
function assignDeep(target, ...sources) {
// 1. 參數(shù)校驗(yàn)
if (target == null) {
throw new TypeError('Cannot convert undefined or null to object');
}
// 2. 如果是基本類型數(shù)據(jù)轉(zhuǎn)為包裝對(duì)象
let result = Object(target);
// 3. 緩存已拷貝過(guò)的對(duì)象毅舆,解決引用關(guān)系丟失問(wèn)題
if (!result['__hash__']) {
result['__hash__'] = new WeakMap();
}
let hash = result['__hash__'];
sources.forEach(v => {
// 4. 如果是基本類型數(shù)據(jù)轉(zhuǎn)為對(duì)象類型
let source = Object(v);
// 5. 遍歷原對(duì)象屬性西篓,基本類型則值拷貝愈腾,對(duì)象類型則遞歸遍歷
Reflect.ownKeys(source).forEach(key => {
// 6. 跳過(guò)自有的不可枚舉的屬性
if (!Object.getOwnPropertyDescriptor(source, key).enumerable) {
return;
}
if (typeof source[key] === 'object' && source[key] !== null) {
// 7. 屬性的沖突處理和拷貝處理
let isPropertyDone = false;
if (!result[key] || !(typeof result[key] === 'object')
|| Array.isArray(result[key]) !== Array.isArray(source[key])) {
// 當(dāng) target 沒有該屬性,或者屬性類型和 source 不一致時(shí)岂津,直接整個(gè)覆蓋
if (hash.get(source[key])) {
result[key] = hash.get(source[key]);
isPropertyDone = true;
} else {
result[key] = Array.isArray(source[key]) ? [] : {};
hash.set(source[key], result[key]);
}
}
if (!isPropertyDone) {
result[key]['__hash__'] = hash;
assignDeep(result[key], source[key]);
}
} else {
Object.assign(result, {[key]: source[key]});
}
});
});
delete result['__hash__'];
return result;
}
上面只給了遞歸版顶滩,存在棧溢出可能性,但基本沒這種對(duì)象層次太深的場(chǎng)景寸爆,想了解其他實(shí)現(xiàn)如循環(huán)版以及詳細(xì)內(nèi)容的礁鲁,可以去我另一篇文章查閱:擴(kuò)展 Object.assign 實(shí)現(xiàn)深拷貝