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

<script>
// 深拷貝實(shí)現(xiàn)流程
// 基礎(chǔ)版
// 如果是前拷貝取胎,我們可以很容易寫出下面的代碼
function clone(target){
let obj = {}
for(const i in target){
obj[i] = target[i]
}
return obj
}

// 如果是深拷貝的話,考慮到我們要拷貝的對象是不知道有多少層深度的辕坝,我們可以用遞歸解決問題誊册,稍微改寫上面的代碼:
// 领突。如果是原始類型,無需繼續(xù)拷貝案怯,直接返回
// 君旦。如果是引用類型,創(chuàng)建一個新的對象嘲碱,遍歷需要克隆的對象金砍,將需要克隆對象的屬性執(zhí)行深拷貝后依次添加到新對象上

function clone2(target){
    if(typeof target === 'object'){
        let cloneTarget = {}
      
        for(const key in target){
            console.log('target', key, target[key])
            cloneTarget[key] = clone2(target[key])
        }
        return cloneTarget
    }else{
        console.log(target, 'finallytarget')
        return target
    }
}

const target = [
    1,2,{
        child:3,
        child2:{
            child3:{
                child4:4
            }
        }
    }
]

console.log(clone2(target), 'target');
// 這是一個最基礎(chǔ)版本的深拷貝,這段代碼可以讓你向面試官展示你可以用遞歸解決問題麦锯,
// 但是還有很多缺陷恕稠,比如數(shù)組

// 只需要稍微改動一下,就可以兼容數(shù)組了

function clone3(target){
    if(typeof target === 'object'){
        let cloneTarget = Array.isArray(target) ? [] : {}
        for(const key in target){
            cloneTarget[key] = clone3(target[key])
        }
        return cloneTarget
    }else{
        return target
    }
}

const target2 = [
    [1,[2,3]],
    {
        parent:{
            child:{
                child2:{
                    child3:4
                }
            }
        }
    }
]

target2.self = target2 // Maximum call stack size exceeded 循環(huán)引用 遞歸進(jìn)入死循環(huán)導(dǎo)致棧內(nèi)存溢出

// console.log(clone3(target2), 'target4444') // 循環(huán)引用報錯

// 循環(huán)引用
// 解決循環(huán)引用問題扶欣,我們可以額外開辟一個存儲空間鹅巍,來存儲當(dāng)前對象和拷貝對象的對應(yīng)關(guān)系千扶,當(dāng)需要拷貝當(dāng)前對象時,先去存儲空間中去找骆捧,
// 如果有的話直接返回澎羞,如果沒有的話繼續(xù)拷貝
// 這樣就巧妙化解循環(huán)陰影的問題

// 這個存儲空間,需要可以存儲key-value形式的數(shù)據(jù)敛苇,且key可以是一個引用類型煤痕,我們可以選擇Map這種數(shù)據(jù)結(jié)構(gòu):
// 。檢查map中有無克隆過對象
// 接谨。有-直接返回
// 摆碉。沒有-將當(dāng)前對象作為key,克隆對象作為value進(jìn)行存儲
// 脓豪。繼續(xù)克隆

function clone4(target,map = new Map()){
    if(typeof target === 'object'){
        let cloneTarget = Array.isArray(target) ? [] : {}
        if(map.get(target)){
            return map.get(target)
        }
        map.set(target, cloneTarget)
        // console.log(map, 'map11111111')
        // for in適合遍歷對象 for of適合用來遍歷具有迭代器的數(shù)組/字符串/map/set等集合
        // for(const [key,vale] of map) 這種解構(gòu)循環(huán)適合用map
        for(const key in target){
            cloneTarget[key] = clone4(target[key],map)
        }
        return cloneTarget
    }else{
        return target
    }
}

console.log(clone4(target2), 'clone4') // 0: (2) [1, Array(2)] 1: {parent: {…}} self: (2) [Array(2), {…}, self: Array(2)]

// 執(zhí)行沒有報錯巷帝,且self屬性的指向正確
// 接下來,可以使用扫夜,WeakMap替代Map來使代碼達(dá)到化龍點(diǎn)睛的作用
function clone5(target,map = new WeakMap()){
   if(typeof target === 'object'){
        let cloneTarget = Array.isArray(target) ? [] : {}
        if(map.get(target)){
            return map.get(target)
        }
        map.set(target, cloneTarget)
        for(let key in target){
            cloneTarget[key] = clone5(target[key], map)
        }
        return cloneTarget
   }else{
       return target
   }
}

console.log(clone5(target2),'clone5')
// WeakMap的作用
// WeakMap對象是一組鍵值對楞泼,其中的鍵是弱引用的。其中的鍵必須是對象笤闯,而值可以是任意的

// 什么是弱引用的呢
// 在計算機(jī)中堕阔,弱引用和強(qiáng)引用相對,是指不能確保其引用的對象不會被垃圾回收器回收的引用颗味。
// 一個對象若是只被弱用用引用超陆,則被認(rèn)為是不可訪問(或弱可訪問)的,并因此可能在任何時刻被回收

// 我們默認(rèn)創(chuàng)建一個對象:const obj={},就默認(rèn)創(chuàng)建了一個強(qiáng)引用的對象浦马,我們只有手動將obj = null时呀,
// 它才會被垃圾回收機(jī)制進(jìn)行回收
// 如果是弱引用對象,垃圾回收機(jī)制會自動幫我們回收

// 舉個例子
// 如果我們使用Map的話晶默,那么對象間存在強(qiáng)引用關(guān)系的
let obj = {name: 'ConardLi'}
const themap = new Map()
themap.set(obj, 'code秘密花園')
// obj = null
// 雖然我們手動將obj進(jìn)行釋放谨娜,但是themap依然對obj存在強(qiáng)引用關(guān)系,所以這部分內(nèi)存依然無法被釋放
// 假設(shè)使用的是WeakMap
const theweapmap = new WeakMap()
theweapmap.set(obj,'code秘密花園')
// 如果是WeakMap的話磺陡,theweapmap和obj存在的就是弱引用關(guān)系趴梢,當(dāng)下一次垃圾回放機(jī)制執(zhí)行時,這塊內(nèi)存就會被釋放掉

// 設(shè)想一下币他,如果我們要拷貝的對象非常龐大時坞靶,使用Map會對內(nèi)存造成非常大的消耗,而且我們需要手動清除Map的屬性才能釋放這塊內(nèi)存圆丹,
// 而WeakMap會幫我們巧妙化解這個問題

// 性能優(yōu)化
// 在上面的代碼中滩愁,我們遍歷數(shù)組和對象都使用了for in這種方式,實(shí)際上for in在遍歷時效率是非常低的辫封,
// 我們來對比下常見的三種循環(huán)for硝枉,while,for in的執(zhí)行效率
// while 4s for 12 for in 141s

// 可以看到倦微,while的效率是最好的妻味,所以我們可以想辦法把for in遍歷改變?yōu)閣hile遍歷

// 我們可以先使用while來實(shí)現(xiàn)一個通用的forEach遍歷,iteratee是遍歷的回調(diào)函數(shù)欣福,他可以接收每次遍歷的value和index兩個參數(shù)
// iteratee 是遍歷的回調(diào)函數(shù)
function foreach(array, iteratee){
    let index = -1
    const length = array.length
    while (++index < length){
        iteratee(array[index], index)
    }
    return array
}

// 下面對我們的clone函數(shù)進(jìn)行改寫:當(dāng)遍歷數(shù)組的時候责球,直接進(jìn)行forEach遍歷,當(dāng)遍歷對象時拓劝,使用Object.keys取出所有的key進(jìn)行遍歷雏逾,
// 然后在遍歷時調(diào)函數(shù)的value當(dāng)key使用
function clone6(target,map=new WeakMap()){
    if(typeof target === [Object]){
        const isArray = Array.isArray(target)
        let cloneTarget = isArray ? [] : {}
        // 將target作為map的鍵值,存儲對應(yīng)的cloneTarget郑临,
        // 在每次遞歸的時候栖博,判斷是否有對應(yīng)的target鍵值
        if(map.get(target)){
            return map.get(target)
        }
        map.set(target, cloneTarget)
        const keys = isArray ? null : Object.keys(target)
        foreach(keys||target,(value,key) => {
            if(keys){
                key = value
            }
            cloneTarget[key] = clone2(target[key],map)
        })
        return  cloneTarget
    }else{
        return target
    }
}

console.log(clone6(target2), 'target6');

// 其他數(shù)據(jù)結(jié)構(gòu)
// 在上面的代碼中,我們其實(shí)只考慮了普通的object和array兩種數(shù)據(jù)類型厢洞,
// 實(shí)際上所有的引用類型遠(yuǎn)遠(yuǎn)不止這兩個仇让,下面我們先嘗試獲取對象的準(zhǔn)確類型

// 合理的類型判斷
// 引用類型指那些可能由多個值構(gòu)成的對象
// 5種基本類型包括:Undefined,Null躺翻,Boolean丧叽,Number和String
// 引用類型的值是保存在內(nèi)存中的對象。
// 與其他語言不同的是公你,Javascript不允許直接訪問內(nèi)存中的位置踊淳,也就是說不能直接操作對象的內(nèi)存空間
// 在操作對象時,實(shí)際上是在操作對象的引用而不是實(shí)際的對象
// 首先陕靠,判斷是否為引用類型嚣崭,我們還需要考慮function和null兩種特殊的數(shù)據(jù)類型
function isObject(target){
    const type = typeof target
    return target !== null&(type === 'object' || type === 'function')
}

// if(!isObject(target)){
//     return target
// }

// 獲取數(shù)據(jù)類型
// 我們可以使用toString來獲取準(zhǔn)確的引用類型
//  每一個數(shù)據(jù)都有toString方法,默認(rèn)情況下懦傍,toString方法被每個Object對象繼承雹舀。
//  如果此方法在自定義對象中未被覆蓋,toString返回'[object type]',其中type是對象的類型

// 注意粗俱,上面提到了如果此方法在自定義對象中未被覆蓋说榆,toString才會達(dá)到預(yù)想的效果,事實(shí)上寸认,大部分引用類型比如Array签财,Date,RegExp等都重寫了
// toString方法
// 我們可以直接調(diào)用Object原型上未被覆蓋的toString方法偏塞,使用call來改變this指向來達(dá)到我們想要的效果

function getType(target){
    return Object.prototype.toString.call(target)
}

// 下面我們可以抽離出一些常用的數(shù)據(jù)類型以便后面使用
const mapTag = '[object Map]'
const setTag = '[object Set]'
const arrayTag = '[object Array]'
const objectTag = '[object Object]'

const boolTag = '[object Boolean]'
const dateTag = '[object Date]'
const errorTag = '[object Error]'
const numberTag = '[object Number]'
const regexpTag = '[object RegExp]'
const stringTag = '[object String]'
const symbolTag = '[object Symbol]'

let deepTag = [mapTag,setTag,arrayTag,objectTag, boolTag,dateTag,errorTag,numberTag,regepTag,stringTag,symbolTag]
// 上面的集中類型中唱蒸,我們簡單將他們分為兩類
//  【牡穑可以繼續(xù)遍歷的對象
//  神汹。不可以繼續(xù)遍歷的對象

// 我們分別作不同的拷貝

// 可繼續(xù)遍歷的類型
// 上面我們已經(jīng)考慮的object庆捺,array都屬于可以繼續(xù)遍歷的對象,因?yàn)樗麄儍?nèi)存都還可以存儲其他數(shù)據(jù)類型的數(shù)據(jù)屁魏,
// 另外滔以,還有Map,Set等都是可以繼續(xù)遍歷的類型氓拼,這里我們只考慮這四種你画,如果你有興趣可以繼續(xù)探索其他類型

// 有序這幾種類型還需要繼續(xù)進(jìn)行遞歸,我們首先需要獲取它們的初始化數(shù)據(jù)桃漾,例如上面的[]和{},
// 我們可以通過拿到constructor的方式來通用的獲取
// 例如:const target={} 就是const target= new Object()的語法糖坏匪。
//      另外,這種方法還有一個好處:因?yàn)槲覀兪褂昧嗽瓕ο蟮臉?gòu)造方法
//      所以它可以保留對象原型上的數(shù)據(jù)
//      如果直接使用普通的{},那么原型必然是丟失的

function getInit(target){
    const Ctor = target.constructor
    return new Ctor()
}

// 下面撬统,我們改寫clone函數(shù)适滓,對可繼續(xù)遍歷的數(shù)據(jù)類型進(jìn)行處理
function clone7(target, map=new WeakMap()){
    // 克隆原始類型
    if(!isObject(target)){
        return target
    }

    // 初始化
    const type = getType(target)
    let cloneTarget
    if(deepTag.includes(type)){
        cloneTarget = getInit(target)
    }
    
    // 防止循環(huán)引用
    if(map.get(target)){
        return map.get(target)
    }
    map.set(target, cloneTarget)

    // 克隆Set
    if(type === setTag){
        target.forEach(value => {
            cloneTarget.add(clone7(value,map))
        })
        return cloneTarget
    }

    // 克隆map
    if(type === mapTag){
        target.forEach((value,key) => {
            cloneTarget.set(key,clone7(value,map))
        })
        return cloneTarget
    }

    // 克隆對象和數(shù)組
    const keys = type === arrayTag ? undefined : Object.keys(target)
    foreach(keys || target,(value, key) => {
        if(keys){
            key = value
        }
        cloneTarget[key] = clone7(target[key], map)
    })
    return target
}

// 執(zhí)行下面的用例進(jìn)行測試
let map = new Map([['a',1],['b',2],[3,'c']])
let set = new Set([1,2,3,6])
const target4 = {
    field1:1,
    field2:undefined,
    field3:{
        child:'child'
    },
    field4:[2,4,8],
    empty:null,
    map,
    set
}

console.log(clone7(target4), 'clone7') // 復(fù)制成功
// 下面繼續(xù)處理其他類型
// 不可繼續(xù)遍歷的類型
// 其他剩余的類型我們把它們統(tǒng)一歸類成不可處理的數(shù)據(jù)類型,我們依次處理
// 這幾種類型我們都可以直接用構(gòu)造函數(shù)和原始數(shù)據(jù)創(chuàng)建一個新對象
function cloneOtherObject(){
    const Ctor = target.constructor
    switch(type){
        case boolTag:
        case numberTag:
        case stringTag:
        case errorTag:
        case dateTag:
            return new Ctor(target)
        case regexpTag:
            return cloneReg(target)
        case symbolTag:
            return cloneSymbol(target)
        default:
            return null    
    }
}

// 克隆Symbol類型
function cloneSymbol(target){
    return Object(Symbol.prototype.valueOf.call(target))
}

//克隆正則
function cloneReg(target){
     const reFlags = /\w*$/
     const result = new target.constructor(target.source, reFlags.exec(target))
     result.lastIndex = target.lastIndex
     return result
}

// 寫到這里宪摧,面試官已經(jīng)看到了你考慮問題的嚴(yán)謹(jǐn)性粒竖,你對變量和類型的理解,
// 對JS API的熟練程度

// 克隆函數(shù)
// 實(shí)際上克隆函數(shù)是沒有實(shí)際應(yīng)用場景的几于,兩個對象使用一個在內(nèi)存中處于同一個地址的函數(shù)是沒有任何問題的
// const isFunc = typeof value == 'function'
// if(isFunc || !cloneableTags[tag]){
//     return object ? value : {}
// }

</script>

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蕊苗,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子沿彭,更是在濱河造成了極大的恐慌朽砰,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,042評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件喉刘,死亡現(xiàn)場離奇詭異瞧柔,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)睦裳,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評論 2 384
  • 文/潘曉璐 我一進(jìn)店門造锅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人廉邑,你說我怎么就攤上這事哥蔚。” “怎么了蛛蒙?”我有些...
    開封第一講書人閱讀 156,674評論 0 345
  • 文/不壞的土叔 我叫張陵糙箍,是天一觀的道長。 經(jīng)常有香客問我牵祟,道長深夯,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,340評論 1 283
  • 正文 為了忘掉前任诺苹,我火速辦了婚禮咕晋,結(jié)果婚禮上雹拄,老公的妹妹穿的比我還像新娘。我一直安慰自己捡需,他們只是感情好办桨,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,404評論 5 384
  • 文/花漫 我一把揭開白布筹淫。 她就那樣靜靜地躺著站辉,像睡著了一般。 火紅的嫁衣襯著肌膚如雪损姜。 梳的紋絲不亂的頭發(fā)上饰剥,一...
    開封第一講書人閱讀 49,749評論 1 289
  • 那天,我揣著相機(jī)與錄音摧阅,去河邊找鬼汰蓉。 笑死,一個胖子當(dāng)著我的面吹牛棒卷,可吹牛的內(nèi)容都是我干的顾孽。 我是一名探鬼主播,決...
    沈念sama閱讀 38,902評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼比规,長吁一口氣:“原來是場噩夢啊……” “哼若厚!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起蜒什,我...
    開封第一講書人閱讀 37,662評論 0 266
  • 序言:老撾萬榮一對情侶失蹤测秸,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后灾常,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體霎冯,經(jīng)...
    沈念sama閱讀 44,110評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年钞瀑,在試婚紗的時候發(fā)現(xiàn)自己被綠了沈撞。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,577評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡雕什,死狀恐怖缠俺,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情监徘,我是刑警寧澤晋修,帶...
    沈念sama閱讀 34,258評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站凰盔,受9級特大地震影響墓卦,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜户敬,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,848評論 3 312
  • 文/蒙蒙 一落剪、第九天 我趴在偏房一處隱蔽的房頂上張望睁本。 院中可真熱鬧,春花似錦忠怖、人聲如沸呢堰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽枉疼。三九已至,卻和暖如春鞋拟,著一層夾襖步出監(jiān)牢的瞬間骂维,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評論 1 264
  • 我被黑心中介騙來泰國打工贺纲, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留航闺,地道東北人。 一個月前我還...
    沈念sama閱讀 46,271評論 2 360
  • 正文 我出身青樓猴誊,卻偏偏與公主長得像潦刃,于是被迫代替她去往敵國和親懈叹。 傳聞我的和親對象是個殘疾皇子乖杠,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,452評論 2 348