不積跬步之手寫深拷貝

深拷貝與淺拷貝.png

淺拷貝和深拷貝的區(qū)別

大家都以為淺拷貝就是把引用類型的值拷貝一份法竞,實際還是引用的同一個對象,把這個叫淺拷貝,實際上這是一個大大的誤會郎楼。

  • 淺拷貝和深拷貝都復(fù)制了值和地址,都是為了解決引用類型賦值后相互影響的問題窒悔。
  • 但是淺拷貝只進(jìn)行一層復(fù)制呜袁,深層次的引用類型還是共享內(nèi)存地址。源對象和拷貝對象還是會相互影響
  • 深拷貝就是無限層級拷貝简珠,深拷貝后的原對象不會和和拷貝對象互相影響阶界。

實現(xiàn)淺拷貝的方式有哪些呢?

  1. Object.assign
  2. 數(shù)組的slice和concat方法
  3. 數(shù)組靜態(tài)方法Array.from
  4. 擴(kuò)展運(yùn)算符

實現(xiàn)深拷貝

要求:

  • 支持對象聋庵、數(shù)組膘融、日期、正則的拷貝祭玉。
  • 處理Map 和 Set
  • 處理原始類型(原始類型直接返回氧映,只有引用類型才有深拷貝這個概念)。
  • 處理 Symbol 作為鍵名的情況脱货。
  • 處理函數(shù)(函數(shù)直接返回岛都,拷貝函數(shù)沒有意義,兩個對象使用內(nèi)存中同一個地址的函數(shù)蹭劈,沒有任何問題)疗绣。
  • 處理 DOM 元素(DOM 元素直接返回,拷貝 DOM 元素沒有意義铺韧,都是指向頁面中同一個)多矮。
  • 額外開辟一個儲存空間 WeakMap,解決循環(huán)引用遞歸爆棧問題(引入 WeakMap 的另一個意義哈打,配合垃圾回收機(jī)制塔逃,防止內(nèi)存泄漏)。

我們要實現(xiàn)的目標(biāo):

const target = {
    a: true,
    b: 100,
    c: 'str',
    d: undefined,
    e: null,
    f:new Date(),
    g: /abc/,
    h:{
        a:'ccc',
        b:12
    },
    i:[1,2,3,4],
    j:Symbol("age"),
    k:new Set([1,2,3,4]),
    l:new Map([[1,2],[3,4]]),
    [nameSymbol]:"job",
};
target.target = target;

1.最簡單的實現(xiàn)料仗,不考慮引用類型

要拷貝的對象

const target = {
      a: true,
      b: 100,
      c: 'str',
      d: undefined,
};

要拷貝這個對象湾盗,我們只需要把對象里面的數(shù)據(jù)按個拷貝出來就可以了。

function deepClone(target){
    let cloneTarget = {};
    for(const key in target){
        cloneTarget[key] = target[key];
    }
    return cloneTarget;
}

2.判斷處理 Null的情況

const target = {
      a: true,
      b: 100,
      c: 'str',
      d: undefined,
      e: null,
};

我們只需要添加一個判斷就可以了立轧。

function deepClone(target){
    //如果是null 就返回
    if(target === null){
        return target;
    }
    let cloneTarget = {};
    for(const key in target){
        cloneTarget[key] = target[key];
    }
    return cloneTarget;
}

3.判斷處理日期的情況

const target = {
    a: true,
    b: 100,
    c: 'str',
    d: undefined,
    e: null,
    f:new Date(),
};

日期是一個對象格粪,我們可以通過日期的構(gòu)造函數(shù)重新new一個對象來進(jìn)行復(fù)制

function deepClone(target){
    //如果是null 就返回
    if(target === null){
        return target;
    }
    if(target instanceof Date){
        return new Date(target);
    }
    let cloneTarget = {};
    for(const key in target){
        cloneTarget[key] = target[key];
    }
    return cloneTarget;
}

4.判斷處理正則的情況

const target = {
    a: true,
    b: 100,
    c: 'str',
    d: undefined,
    e: null,
    f:new Date(),
    g: /abc/,
};

正則和日期一樣躏吊,同樣也可以使用構(gòu)造函數(shù)來處理。

function deepClone(target){
    //如果是null 就返回
    if(target === null){
        return target;
    }
    if(target instanceof Date){
        return new Date(target);
    }
    if(target instanceof RegExp){
        return new RegExp(target);
    }
    let cloneTarget = {};
    for(const key in target){
        cloneTarget[key] = target[key];
    }
    return cloneTarget;
}

5.深拷貝復(fù)制引用類型

const target = {
    a: true,
    b: 100,
    c: 'str',
    d: undefined,
    e: null,
    f:new Date(),
    g: /abc/,
    h:{
        a:'ccc',
        b:12
    }
};

里面既然有了引用類型帐萎,那么我們只需要遞歸調(diào)用一下比伏,然后返回就可以了。
arguments.callee 這里指向函數(shù)的引用疆导。

function deepClone(target){
    
    //如果是null 就返回
    if(target === null){
        return target;
    }
    if(target instanceof Date){
        return new Date(target);
    }
    if(target instanceof RegExp){
        return new RegExp(target);
    }
    
    //處理引用類型 以免死循環(huán)
    if(typeof target !== 'object'){
        return target;
    }
   
    const cloneTarget = {} 
    for(const key in target){
        cloneTarget[key] = deepClone(target[key]);
    }
    return cloneTarget;
}

6.處理數(shù)組的情況

const target = {
    a: true,
    b: 100,
    c: 'str',
    d: undefined,
    e: null,
    f:new Date(),
    g: /abc/,
    h:{
        a:'ccc',
        b:12
    },
    i:[1,2,3,4]
};

數(shù)組這了比較簡單赁项,它的區(qū)別僅僅只是我們拷貝的是對象還是數(shù)組。

function deepClone(target){
    
    //如果是null 就返回
    if(target === null){
        return target;
    }
    if(target instanceof Date){
        return new Date(target);
    }
    if(target instanceof RegExp){
        return new RegExp(target);
    }
    //處理引用類型 以免死循環(huán)
    if(typeof target !== 'object'){
        return target;
    }
    // 處理對象和數(shù)組 以及原型鏈
    const cloneTarget = new target.constructor() // 創(chuàng)建一個新的克隆對象或克隆數(shù)組
    for(const key in target){
        cloneTarget[key] = deepClone(target[key]);
    }
    return cloneTarget;
}

可以看到上面有一個騷操作澈段,就是我們通過實例的constructor拿到它的構(gòu)造函數(shù)悠菜,然后直接new 就可以了。
這樣就不用在去判斷是否是數(shù)組還是對象败富,然后去調(diào)用它對應(yīng)的構(gòu)造函數(shù)悔醋,當(dāng)然這里這樣寫有一定的風(fēng)險。如果作為底層庫來使用囤耳,需要考慮constructor并沒有指向它構(gòu)造函數(shù)的情況篙顺。

不過通過它的構(gòu)造函數(shù)我們解決了另外一個問題偶芍,就是原型鏈充择。通過它原本的構(gòu)造函數(shù),原型鏈自然也是完整保存的匪蟀。意想不到的小細(xì)節(jié)

7.處理Symbol的情況

const target = {
    a: true,
    b: 100,
    c: 'str',
    d: undefined,
    e: null,
    f:new Date(),
    g: /abc/,
    h:{
        a:'ccc',
        b:12
    },
    i:[1,2,3,4],
    j:Symbol("age"),
    [Symbol("name")]:"job",
};

Symbol的特性就是全局唯一值椎麦,里面的參數(shù)只是一個描述符。而并不是具體值材彪。這里在處理的時候观挎,我們需要考慮兩種情況。

  1. 一種是作為值存在的Symbol
  2. 第二種是作為鍵存在的Symbol

作為值存在的話段化,我們可以通過Symbol.prototype.toString方法拿到它的描述字段嘁捷。然后重新構(gòu)造一個Symbol

//處理值為Symbol的情況
    if(typeof target === 'symbol'){
        return Symbol(target.toString());
    }

作為鍵存在的話,for in的遍歷范圍就無法滿足我們的要求的显熏,所以需要換成遍歷范圍更加合適的Reflect.ownKeys()雄嚣。

下面是MDN的原話

Reflect.ownKeys 方法返回一個由目標(biāo)對象自身的屬性鍵組成的數(shù)組。
它的返回值等同于Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))喘蟆。

同時使用Reflect.ownKeys解決了for in的一個隱藏的問題缓升。那就是會把原型對象的屬性也遍歷下來,然后存儲到拷貝對象里面蕴轨。

升級后的效果:


function deepClone(target,map = new WeakMap()){
    //如果是null 就返回
    if(target === null){
        return target;
    }
    //處理日期
    if(target instanceof Date){
        return new Date(target);
    }
    //處理正則表達(dá)式
    if(target instanceof RegExp){
        return new RegExp(target);
    }
    //處理值為Symbol的情況
    if(typeof target === 'symbol'){
        return Symbol(target.toString());
    }
    //處理引用類型 以免死循環(huán)
    if(typeof target !== 'object'){
        return target;
    }
    // 處理對象和數(shù)組
    const cloneTarget = new target.constructor() // 創(chuàng)建一個新的克隆對象或克隆數(shù)組
    //通過Reflect來拿到所有可枚舉和不可枚舉的屬性港谊,以及Symbol的屬性。
    Reflect.ownKeys(target).forEach(key=>{
        cloneTarget[key] = deepClone(target[key]);
    })
    return cloneTarget;
}

8.處理循環(huán)引用的情況

const target = {
    a: true,
    b: 100,
    ...
};

target.target = target;

數(shù)據(jù)是上面的這樣橙弱。這種的數(shù)據(jù)會讓遞歸進(jìn)入死循環(huán)從而造成爆棧的問題歧寺。
這里可以通過WeakMap來建立當(dāng)前對象和拷貝對象的弱引用關(guān)系燥狰,判斷當(dāng)前對象是否存在,如果存在就使用它
保存的對象斜筐,如果不存在就添加進(jìn)去碾局。

WeakMap的原理是,它的鍵值只能是引用類型奴艾,它的這個引用類型并不會強(qiáng)制標(biāo)記净当,當(dāng)垃圾回收機(jī)制需要釋放內(nèi)存的時候,它會被直接釋放蕴潦,而不需要做其他的操作像啼,使用者也不擔(dān)心內(nèi)存泄漏的問題。

我們用WeakMap改造升級一下潭苞。

function deepClone(target,map = new WeakMap()){
    //如果是null 就返回
    if(target === null){
        return target;
    }
    //處理日期
    if(target instanceof Date){
        return new Date(target);
    }
    //處理正則表達(dá)式
    if(target instanceof RegExp){
        return new RegExp(target);
    }
    //處理值為Symbol的情況
    if(typeof target === 'symbol'){
        return Symbol(target.toString());
    }
    //處理引用類型 以免死循環(huán)
    if(typeof target !== 'object'){
        return target;
    }
    //判斷釋放存在
    if(map.has(target)){
        return target;
    }
    // 處理對象和數(shù)組
    const cloneTarget = new target.constructor() // 創(chuàng)建一個新的克隆對象或克隆數(shù)組
    //保存原引用和拷貝引用的關(guān)系
    map.set(target,cloneTarget)
    //通過Reflect來拿到所有可枚舉和不可枚舉的屬性忽冻,以及Symbol的屬性。
    Reflect.ownKeys(target).forEach(key=>{
        cloneTarget[key] = deepClone(target[key],map);
    })
    return cloneTarget;
}

這樣就可以處理循環(huán)引用的問題了此疹。

9.處理 Set和Map的情況 和HTMLElement的情況

因為這兩個都是可迭代的數(shù)據(jù)結(jié)構(gòu)僧诚,同時它們又有自己的添加屬性的方法。所以需要按個判斷蝗碎。

由于之前 const cloneTarget = new target.constructor()湖笨,我們并不需要去手動添加處理MapSet

而HTMLElement的情況并不需要處理蹦骑,拷貝也沒有意義慈省,直接返回就好。

function deepClone(target,map = new WeakMap()){
    //如果是null 就返回
    if(target === null) return target;
    //處理日期
    if(target instanceof Date)  return new Date(target);
    //處理正則表達(dá)式
    if(target instanceof RegExp)  return new RegExp(target);
    //處理值為Symbol的情況
    if(typeof target === 'symbol') return Symbol(target.toString());
    // 處理 DOM元素
    if (target instanceof HTMLElement) return target
    //非引用類型的直接返回 比如函數(shù) 就不需要處理眠菇,直接返回就好
    if(typeof target !== 'object') return target;
    //從緩沖中讀取
    if(map.has(target)) return target;
    // 處理對象和數(shù)組
    const cloneTarget = new target.constructor() // 創(chuàng)建一個新的克隆對象或克隆數(shù)組
    //保存原引用和拷貝引用的關(guān)系
    map.set(target,cloneTarget)
    //處理Map的情況
    if(target instanceof Map){
       for(let [key,value] of target){
           target.set(key,deepClone(value,map));
       }
       return target;
    }
    //處理set的情況
    if(target instanceof Set){
        for(let value of target){
            target.add(deepClone(value,map))
        }
        return target;
    }
    //通過Reflect來拿到所有可枚舉和不可枚舉的屬性边败,以及Symbol的屬性。
    Reflect.ownKeys(target).forEach(key=>{
        cloneTarget[key] = deepClone(target[key],map);
    })
    return cloneTarget;
}

差不多就是這樣了捎废。我們處理了下面的情況:

const target = {
    a: true,
    b: 100,
    c: 'str',
    d: undefined,
    e: null,
    f:new Date(),
    g: /abc/,
    h:{
        a:'ccc',
        b:12
    },
    i:[1,2,3,4],
    j:Symbol("age"),
    k:new Set([1,2,3,4]),
    l:new Map([[1,2],[3,4]]),
    [nameSymbol]:"job",
};
target.target = target;

打印結(jié)果:

{
  a: true,
  b: 100,
  c: 'str',
  d: undefined,
  e: null,
  f: 2022-03-30T09:47:13.762Z,
  g: /abc/,
  h: { a: 'ccc', b: 12 },
  i: [ 1, 2, 3, 4 ],
  j: Symbol(Symbol(age)),
  k: Set(4) { 1, 2, 3, 4 },
  l: Map(2) { 1 => 2, 3 => 4 },
  target: <ref *1> {
    a: true,
    b: 100,
    //這里省略折起...
  },
  [Symbol(name)]: 'job'
}

如果真的要實現(xiàn)一個深拷貝 笑窜,那么情況要復(fù)雜的多,可以參考lodash的源碼學(xué)習(xí)登疗。

聊一下另外一個深拷貝的方式: JSON.parse(JSON.stringify(target))

我們使用這個來實現(xiàn)深拷貝 排截,直接深拷貝上面的測試用例,看看能夠有幾個谜叹?

const target = {
    0:NaN,
    1:Infinity,
    2:-Infinity,
    a: true,
    b: 100,
    c: 'str',
    d: undefined,
    e: null,
    f:new Date(),
    g: /abc/,
    h:{
        a:'ccc',
        b:12
    },
    i:[1,2,3,4],
    j:Symbol("age"),
    k:new Set([1,2,3,4]),
    l:new Map([[1,2],[3,4]]),
    [nameSymbol]:"job",
};
target.target = target;
JSON.parse(JSON.stringify(target))
//報錯:
VM8398:1 Uncaught TypeError: Converting circular structure to JSON
    --> starting at object with constructor 'Object'
    --- property 'target' closes the circle
    at JSON.stringify (<anonymous>)
    at <anonymous>:1:17

意思循環(huán)引用錯誤匾寝。我們把循環(huán)引用的代碼去掉:

target.target = target;

看一下打印出來的結(jié)果:

'0': null,
'1': null,
'2': null,
a: true
b: 100
c: "str"
e: null
f: "2022-03-30T11:20:08.362Z"
g: {}
h: {a: 'ccc', b: 12}
i: (4) [1, 2, 3, 4]
k: {}
l: {}
[[Prototype]]: Object

從頭看到尾:

  1. d: undefined,沒有了,無法處理值 為undefined的情況
  2. f:2022-03-30T11:20:08.362Z時間變成了字符串
  3. g: {} 正則表達(dá)式 沒有了
  4. j:Symbol("age") 所有Symbol的值都沒有了荷腊,
  5. k:new Set([1,2,3,4]) Set 沒有了
  6. l:new Map([[1,2],[3,4]]) Map 沒有了

會忽略的有 : undefined,Symbol,函數(shù) ,直接不存在
會變成對象:Map,Set,正則表達(dá)式
會被序列化為Null:NaN艳悔、Infinity-Infinity

不能循環(huán)引用女仰。

學(xué)習(xí)參考的文章:感謝這些大佬猜年,我才能站在巨人的肩膀上抡锈。
輕松拿下 JS 淺拷貝、深拷貝
如何寫出一個驚艷面試官的深拷貝?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末乔外,一起剝皮案震驚了整個濱河市床三,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌杨幼,老刑警劉巖撇簿,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異差购,居然都是意外死亡四瘫,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門欲逃,熙熙樓的掌柜王于貴愁眉苦臉地迎上來找蜜,“玉大人,你說我怎么就攤上這事稳析∠醋觯” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵彰居,是天一觀的道長诚纸。 經(jīng)常有香客問我,道長裕菠,這世上最難降的妖魔是什么咬清? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮奴潘,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘影钉。我一直安慰自己画髓,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布平委。 她就那樣靜靜地躺著奈虾,像睡著了一般。 火紅的嫁衣襯著肌膚如雪廉赔。 梳的紋絲不亂的頭發(fā)上肉微,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機(jī)與錄音蜡塌,去河邊找鬼碉纳。 笑死,一個胖子當(dāng)著我的面吹牛馏艾,可吹牛的內(nèi)容都是我干的劳曹。 我是一名探鬼主播奴愉,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼铁孵!你這毒婦竟也來了锭硼?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤蜕劝,失蹤者是張志新(化名)和其女友劉穎檀头,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體岖沛,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡鳖擒,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了烫止。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蒋荚。...
    茶點(diǎn)故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖馆蠕,靈堂內(nèi)的尸體忽然破棺而出期升,到底是詐尸還是另有隱情,我是刑警寧澤互躬,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布播赁,位于F島的核電站,受9級特大地震影響吼渡,放射性物質(zhì)發(fā)生泄漏容为。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一寺酪、第九天 我趴在偏房一處隱蔽的房頂上張望坎背。 院中可真熱鬧,春花似錦寄雀、人聲如沸得滤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽懂更。三九已至,卻和暖如春急膀,著一層夾襖步出監(jiān)牢的瞬間沮协,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工卓嫂, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留慷暂,地道東北人。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓命黔,卻偏偏與公主長得像呜呐,于是被迫代替她去往敵國和親就斤。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評論 2 355

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