手寫實(shí)現(xiàn)深度拷貝

手寫實(shí)現(xiàn)深度拷貝

本文參考:面試題之如何實(shí)現(xiàn)一個(gè)深拷貝

基礎(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ō):

  1. 從樹根節(jié)點(diǎn)開始,遍歷它的第一層子節(jié)點(diǎn)套啤,把這些節(jié)點(diǎn)都放入椏砥或隊(duì)列中,結(jié)束本次循環(huán)潜沦;
  2. 下次循環(huán)開始抹竹,取出棧頂或隊(duì)頭節(jié)點(diǎn)處理:若該節(jié)點(diǎn)還有子節(jié)點(diǎn),那么遍歷取出所有子節(jié)點(diǎn)止潮,放入椙耘校或隊(duì)列中,結(jié)束本次循環(huán)喇闸;
  3. 重復(fù)第2步袄琳,直至棧或隊(duì)列中無(wú)節(jié)點(diǎn)燃乍;
  4. 如果是用棧輔助唆樊,則對(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)深拷貝

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市赁豆,隨后出現(xiàn)的幾起案子仅醇,更是在濱河造成了極大的恐慌,老刑警劉巖魔种,帶你破解...
    沈念sama閱讀 219,490評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件析二,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡节预,警方通過(guò)查閱死者的電腦和手機(jī)叶摄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)安拟,“玉大人蛤吓,你說(shuō)我怎么就攤上這事】飞猓” “怎么了会傲?”我有些...
    開封第一講書人閱讀 165,830評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)拙泽。 經(jīng)常有香客問(wèn)我淌山,道長(zhǎng),這世上最難降的妖魔是什么顾瞻? 我笑而不...
    開封第一講書人閱讀 58,957評(píng)論 1 295
  • 正文 為了忘掉前任泼疑,我火速辦了婚禮,結(jié)果婚禮上荷荤,老公的妹妹穿的比我還像新娘退渗。我一直安慰自己,他們只是感情好梅猿,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,974評(píng)論 6 393
  • 文/花漫 我一把揭開白布氓辣。 她就那樣靜靜地躺著,像睡著了一般袱蚓。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上几蜻,一...
    開封第一講書人閱讀 51,754評(píng)論 1 307
  • 那天喇潘,我揣著相機(jī)與錄音体斩,去河邊找鬼。 笑死颖低,一個(gè)胖子當(dāng)著我的面吹牛絮吵,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播忱屑,決...
    沈念sama閱讀 40,464評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼蹬敲,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了莺戒?” 一聲冷哼從身側(cè)響起伴嗡,我...
    開封第一講書人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎从铲,沒想到半個(gè)月后瘪校,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,847評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡名段,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,995評(píng)論 3 338
  • 正文 我和宋清朗相戀三年阱扬,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片伸辟。...
    茶點(diǎn)故事閱讀 40,137評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡麻惶,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出信夫,到底是詐尸還是另有隱情用踩,我是刑警寧澤,帶...
    沈念sama閱讀 35,819評(píng)論 5 346
  • 正文 年R本政府宣布忙迁,位于F島的核電站脐彩,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏姊扔。R本人自食惡果不足惜惠奸,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,482評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望恰梢。 院中可真熱鬧佛南,春花似錦、人聲如沸嵌言。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)摧茴。三九已至绵载,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背娃豹。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工焚虱, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人懂版。 一個(gè)月前我還...
    沈念sama閱讀 48,409評(píng)論 3 373
  • 正文 我出身青樓鹃栽,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親躯畴。 傳聞我的和親對(duì)象是個(gè)殘疾皇子民鼓,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,086評(píng)論 2 355

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