擴展 Object.assign 實現(xiàn)深拷貝

擴展 Object.assign 實現(xiàn)深拷貝

本文參考: Object.assign 原理及其實現(xiàn)

需求場景

上一篇文章:手寫實現(xiàn)深拷貝中疼蛾,我們講了淺拷貝和深拷貝趴泌,也實現(xiàn)了深拷貝方案薯鳍。

但深拷貝富寿,它是基于一個原對象见秽,完完整整拷貝一份新對象出來盔然,假如我們的需求是要將原對象上的屬性完完整整拷貝到另外一個已存在的對象上桅打,這時候深拷貝就有點無能為力了。

就有點類似于 Object.assign():

var a = {
    a: 1,
    b: 2,
    c: {
        a: 1
    }
}

var o = Object.assign(a, {a: 2, c: {b: 2}, d: 3});
o; // {a: 2, b: 2, c: {b: 2}, d: 3}

將一個原對象上的屬性拷貝到另一個目標對象上愈案,最終結(jié)果取兩個對象的并集挺尾,如果有沖突的屬性,則以原對象上屬性為主站绪,表現(xiàn)上就是直接覆蓋過去遭铺,這是 Object.assign() 方法的用途。

但很可惜的是恢准,Object.assign 只是淺拷貝魂挂,它只處理第一層屬性,如果屬性是基本類型馁筐,則值拷貝涂召,如果是對象類型,則引用拷貝敏沉,如果有沖突果正,則整個覆蓋過去。

這往往不符合我們的需求場景盟迟,講個實際中常接觸的場景:

在一些表單操作頁面秋泳,頁面初始化時可能會先前端本地創(chuàng)建一個對象來存儲表單項,對象中可能會有一些初始值攒菠,然后訪問了后臺接口轮锥,讀取當前頁的表單數(shù)據(jù),后臺返回了 json 對象要尔,這時候我們希望當前頁的表單存儲對象應(yīng)該是后臺返回的 json 對象和初始創(chuàng)建的對象的并集舍杜,有沖突以后臺返回的為主,如:

var a = {
    a: {
        a: 1
    }
}

var o = {
    a: {
        b: 2
    }
}

// 我們希望得到的是:
{
    a: {
        a: 1, 
        b: 2
    }
}

Object.assign(a, b);  // {a: {b: 2}}

其實赵辕,說白了既绩,這種需求就是希望可以進行深拷貝,而且是深拷貝到一個目標對象上还惠。

上一篇的深拷貝方案雖然可以實現(xiàn)深度拷貝饲握,但卻不支持拷貝到一個目標對象上,而 Object.assign 雖然支持拷貝到目標對象上,但它只是淺拷貝救欧,只處理第一層屬性的拷貝衰粹。所以,兩種方案都不適用于該場景笆怠。

但兩種方案結(jié)合一下铝耻,其實也就是該需求的實現(xiàn)方案了,所以要么擴展深拷貝方案蹬刷,增加與目標對象屬性的交集處理和沖突處理瓢捉;要么擴展 Object.assign,讓它支持深拷貝办成。

實現(xiàn)方案

本篇就選擇基于 Object.assign泡态,擴展支持深拷貝:assignDeep。

這里同樣會給出幾個方案迂卢,因為深拷貝的實現(xiàn)可以用遞歸某弦,也可以用循環(huán),遞歸比較好寫而克、易懂刀崖,但有棧溢出問題;循環(huán)比較難寫拍摇,但沒有棧溢出問題亮钦。

遞歸版

function assignDeep(target, ...sources) {
    // 1. 參數(shù)校驗
    if (target == null) {
        throw new TypeError('Cannot convert undefined or null to object');
    }

    // 2. 如果是基本類型數(shù)據(jù)轉(zhuǎn)為包裝對象
    let result = Object(target);
    
    // 3. 緩存已拷貝過的對象,解決引用關(guān)系丟失問題
    if (!result['__hash__']) {
        result['__hash__'] = new WeakMap();
    }
    let hash  = result['__hash__'];

    sources.forEach(v => {
        // 4. 如果是基本類型數(shù)據(jù)轉(zhuǎn)為對象類型
        let source = Object(v);
        // 5. 遍歷原對象屬性充活,基本類型則值拷貝蜂莉,對象類型則遞歸遍歷
        Reflect.ownKeys(source).forEach(key => {
            // 6. 跳過自有的不可枚舉的屬性
            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])) {
                    // 當 target 沒有該屬性,或者屬性類型和 source 不一致時混卵,直接整個覆蓋
                    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;
}

要注意的地方映穗,其實也就是模擬實現(xiàn) Object.assign 的一些細節(jié)處理,比如參數(shù)校驗幕随,參數(shù)處理蚁滋,屬性遍歷,以及引用關(guān)系丟失問題赘淮。

循環(huán)版

function assignDeep(target, ...sources) {
    // 1. 參數(shù)校驗
    if (target == null) {
        throw new TypeError('Cannot convert undefined or null to object');
    }

    // 2. 如果是基本類型辕录,則轉(zhuǎn)換包裝對象
    let result = Object(target);
    // 3. 緩存已拷貝過的對象
    let hash = new WeakMap();
    
    // 4. 目標屬性是否可直接覆蓋賦值判斷
    function canPropertyCover(node) {
        if (!node.target[node.key]) {
            return true;
        }
        if (node.target[node.key] == null) {
            return true;
        }
        if (!(typeof node.target[node.key] === 'object')) {
            return true;
        }
        if (Array.isArray(node.target[node.key]) !== Array.isArray(node.data)) {
            return true;
        }
        return false;
    }
    
    sources.forEach(v => {
        let source = Object(v);
        
        let stack = [{
            data: source,
            key: undefined,
            target: result
        }];

        while(stack.length > 0) {
            let node = stack.pop();
            if (typeof node.data === 'object' && node.data !== null) {
                let isPropertyDone = false;
                if (hash.get(node.data) && node.key !== undefined) {
                    if (canPropertyCover(node)) {
                        node.target[node.key] = hash.get(node.data);
                        isPropertyDone = true;
                    }
                }
                
                if(!isPropertyDone) {
                    let target;
                    if (node.key !== undefined) {
                        if (canPropertyCover(node)) {
                            target = Array.isArray(node.data) ? [] : {};
                            hash.set(node.data, target);
                            node.target[node.key] = target;
                        } else {
                            target = node.target[node.key];
                        }
                    } else {
                        target = node.target;
                    }
                    
                    Reflect.ownKeys(node.data).forEach(key => {
                        // 過濾不可枚舉屬性
                        if (!Object.getOwnPropertyDescriptor(node.data, key).enumerable) {
                            return;
                        }
                        stack.push({
                            data: node.data[key],
                            key: key,
                            target: target
                        });
                    });
                }
            } else {
                Object.assign(node.target, {[node.key]: node.data});
            }
        }

    });

    return result;
}

測試用例:

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()
}

o.l = o;
var o1 = assignDeep({}, {m: {b: 2}, n: 1}, o, {n: {a: 1}});

上面的方案仍舊不是100%完美,仍舊存在一些不足:

  • 沒有考慮 ES6 的 set梢卸,Map 等新的數(shù)據(jù)結(jié)構(gòu)類型
  • get走诞,set 存取器邏輯無法拷貝
  • 沒有考慮屬性值是內(nèi)置對象的場景,比如 /sfds/ 正則蛤高,或 new Date() 日期這些類型的數(shù)據(jù)
  • 為了解決循環(huán)引用和引用關(guān)系丟失問題而加入的 hash 緩存無法識別一些屬性沖突場景蚣旱,導(dǎo)致同時存在沖突和循環(huán)引用時碑幅,拷貝的結(jié)果可能有誤
  • 等等未發(fā)現(xiàn)的邏輯問題坑

雖然有一些小問題,但基本適用于大多數(shù)場景了塞绿,出問題時再想辦法慢慢填坑沟涨,目前這樣足夠使用了,而且异吻,當目標對象是空對象時裹赴,此時也可以當做深拷貝來使用。

當然涧黄,也歡迎指點一下篮昧。

TypeScript 業(yè)務(wù)版

根據(jù)實際項目中的業(yè)務(wù)需求赋荆,進行的相關(guān)處理笋妥,就沒必要像上面的通用版考慮那么多細節(jié),比如我項目中使用 ts 開發(fā)窄潭,業(yè)務(wù)需求是要解決實體類數(shù)據(jù)的初始化和服務(wù)端返回的實體類的交集合并場景春宣。

另外,只有對象類型的屬性需要進行交集處理嫉你,其余類型均直接覆蓋即可:

/**
【需求場景】:
export class ADomain {
    name: string = 'dasu';
    wife: B[] = [];
    type: number;
}

export class B {
    count: number = 0;
}

xxxDomain: ADomain;
xxxService.getXXX().subscript(json => {
    this.xxxDomain = json;
    if (!this.xxxDomain.wife) { // 這個處理很繁瑣
        this.xxxDomain.wife = [];
    }
});

假設(shè)變量 xxxDomain 為實體類 ADomain 實例月帝,實體類內(nèi)部對其各字段設(shè)置了一些初始值;
但由于 xxxService 從后端接口拿到數(shù)據(jù)后幽污, json 對象可能并不包含 wife 字段嚷辅,
這樣當將 xxxDomain = json 賦值后,后續(xù)再使用到 xxxDomain.wife 時還得手動進行判空處理距误,
這種方式太過繁瑣簸搞,一旦實體結(jié)構(gòu)復(fù)雜一點,層次深一點准潭,判空邏輯會特別長趁俊,特別亂,特別煩

(后端不負責(zé)初始化刑然,而之所以某些字段需要初始化寺擂,是因為界面上需要該值進行呈現(xiàn))

基于該需求場景,封裝了這個工具類:
【使用示例】:
xxxService.getXXX().subscript(json => {
    DomainUtils.handleUndefined(json, ADomain);
    this.xxxDomain = json;
});
*/
export class DomainUtils {
    /**
    * 接收兩個參數(shù)泼掠,第一個是服務(wù)端返回的 json 對象怔软,第二個是該對象對應(yīng)的 class 類,內(nèi)部會自動根據(jù) class 創(chuàng)建一個新的空對象择镇,然后跟 json 對象的每個屬性兩兩比較爽雄,如果在新對象中發(fā)現(xiàn)有某個字段有初始值,但 json 對象上沒有沐鼠,則復(fù)制過去挚瘟。
    */
    static handleUndefined(domain: object, prop) {
        let o = new prop();
        if (Array.isArray(domain)) {
            domain.forEach(value => {
                DomainUtils._clone(domain, o);
            });
        } else {
            DomainUtils._clone(domain, o);
        }
        return domain;
    }
    
    private static _clone(target: object, source: object) {
        Object.keys(source).forEach(value => {
            if (!Array.isArray(source[value]) && typeof source[value] === 'object' && source[value] !== null) {
                if (target[value] == null) {
                    target[value] = source[value];
                } else {
                    DomainUtils._clone(target[value] as object, source[value] as object);
                }
            } else {
                if (target[value] == null) {
                    target[value] = source[value];
                }
            }
        });
    }
}

因為直接基于業(yè)務(wù)需求場景來進行的封裝叹谁,所以我很明確參數(shù)的結(jié)構(gòu)是什么,使用的場景是什么乘盖,很多細節(jié)就沒處理了焰檩,比如參數(shù)的校驗等。

而且订框,這個目的在于解決初始化問題析苫,所以并不是一個深克隆,而是直接在原對象上進行操作穿扳,等效于將初始化的值都復(fù)制到原對象上衩侥,如果原對象同屬性沒有值的時候。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末矛物,一起剝皮案震驚了整個濱河市茫死,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌履羞,老刑警劉巖峦萎,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異忆首,居然都是意外死亡爱榔,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門糙及,熙熙樓的掌柜王于貴愁眉苦臉地迎上來详幽,“玉大人,你說我怎么就攤上這事浸锨〈狡福” “怎么了?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵揣钦,是天一觀的道長雳灾。 經(jīng)常有香客問我,道長冯凹,這世上最難降的妖魔是什么谎亩? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮宇姚,結(jié)果婚禮上匈庭,老公的妹妹穿的比我還像新娘。我一直安慰自己浑劳,他們只是感情好阱持,可當我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著魔熏,像睡著了一般衷咽。 火紅的嫁衣襯著肌膚如雪鸽扁。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天镶骗,我揣著相機與錄音桶现,去河邊找鬼。 笑死鼎姊,一個胖子當著我的面吹牛骡和,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播相寇,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼慰于,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了唤衫?” 一聲冷哼從身側(cè)響起婆赠,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎战授,沒想到半個月后页藻,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體桨嫁,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡植兰,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了璃吧。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片楣导。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖畜挨,靈堂內(nèi)的尸體忽然破棺而出筒繁,到底是詐尸還是另有隱情,我是刑警寧澤巴元,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布毡咏,位于F島的核電站,受9級特大地震影響逮刨,放射性物質(zhì)發(fā)生泄漏呕缭。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一修己、第九天 我趴在偏房一處隱蔽的房頂上張望恢总。 院中可真熱鬧,春花似錦睬愤、人聲如沸片仿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽砂豌。三九已至厢岂,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間阳距,已是汗流浹背咪笑。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留娄涩,地道東北人窗怒。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像蓄拣,于是被迫代替她去往敵國和親扬虚。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,722評論 2 345

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

  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴謹 對...
    cosWriter閱讀 11,089評論 1 32
  • 手寫實現(xiàn)深度拷貝 本文參考:面試題之如何實現(xiàn)一個深拷貝 基礎(chǔ)理論 拷貝的基礎(chǔ)是賦值球恤,在 js 中辜昵,將一個變量賦值給...
    請叫我大蘇閱讀 968評論 0 1
  • 屬性的簡潔表示法 ES6允許直接寫入變量和函數(shù),作為對象的屬性和方法咽斧。這樣的書寫更加簡潔堪置。 上面代碼表明,ES6允...
    呼呼哥閱讀 2,906評論 0 2
  • There was times when i doubted myself, I deeply appreciat...
    七月上的冥王星閱讀 206評論 0 0
  • 1.JavaScript 是 web 上一種功能強大的編程語言 张惹,用于開發(fā)交互式 的 web 頁面舀锨。它不需要進行 ...
    路人愛早茶閱讀 228評論 0 0