JavaScript系列: 1、手撕JS中的深淺拷貝

文章首發於掘金, 地址JavaScript系列: 1、手撕JS中的深淺拷貝面試

前言

面試官:如何手寫一個深拷貝?
應聘者: JSON.parse(JSON.stringify(object))
面試官:有什麼優缺點?
應聘者:不能序列化函數, 忽略undefined...
面試官:有其餘方法? 怎麼解決循環引用?
應聘者:for循環 。。。數組

1、數據類型

一、基本數據類型

Number、String、Boolean、Null、undefined、Symbol、Bigint函數

Bigint 是最近新引入的基本數據類型post

二、引用數據類型

Object、Array、Function等性能

數據類型不是本文重點, 重點是實現深淺拷貝this

下面是要copy的對象, 以後的代碼都會直接使用$obj, 以後不會再次聲明
// lmran
var $obj = {
    func: function () {
        console.log('this is function')
    },
    date: new Date(),    
    symbol: Symbol(),
    a: null,
    b: undefined,
    c: {
        a: 1
    },
    e: new RegExp('regexp'),
    f: new Error('error')
}

$obj.c.d = $obj

2、 淺拷貝

一、什麼是淺拷貝

一句話能夠說就是:對對象而言,它的第一層屬性值若是是基本數據類型則徹底拷貝一份數據,若是是引用類型就拷貝內存地址。確實拷貝的很淺[偷笑]prototype

二、實現

Object.assign()

// lmran
let obj1 = {
    name: 'yang',
    res: {
        value: 123
    }
}

let obj2 = Object.assign({}, obj1)
obj2.res.value = 456
console.log(obj2) // {name: "haha", res: {value: 456}}
console.log(obj1) // {name: "haha", res: {value: 456}}
obj2.name = 'haha'
console.log(obj2) // {name: "haha", res: {value: 456}}
console.log(obj1) // {name: "yang", res: {value: 456}}

展開語法 Spread

// lmran
let obj1 = {
    name: 'yang',
    res: {
        value: 123
    }
}

let {...obj2} = obj1
obj2.res.value = 456
console.log(obj2) // {name: "haha", res: {value: 456}}
console.log(obj1) // {name: "haha", res: {value: 456}}
obj2.name = 'haha'
console.log(obj2) // {name: "haha", res: {value: 456}}
console.log(obj1) // {name: "yang", res: {value: 456}}

Array.prototype.slice

// lmran
 const arr1 = [
     'yang',
     {
         value: 123
     }
 ];
 
 const arr2 = arr1.slice(0);
 arr2[1].value = 456;
 console.log(arr2); // ["yang", {value: 456}]
 console.log(arr1); // ["yang", {value: 456}]
 arr2[0] = 'haha';
 console.log(arr2); // ["haha", {value: 456}]
 console.log(arr1); // ["yang", {value: 456}]

Array.prototype.concat

// lmran
  const arr1 = [
      'yang',
      {
          value: 123
      }
  ];
  
  const arr2 = [].concat(arr1);
  arr2[1].value = 456;
  console.log(arr2); // ["yang", {value: 456}]
  console.log(arr1); // ["yang", {value: 456}]
  arr2[0] = 'haha';
  console.log(arr2); // ["haha", {value: 456}]
  console.log(arr1); // ["yang", {value: 456}]
實際上對於數組來講, 只要不修改原數組, 從新返回一個新數組就能夠實現淺拷貝,好比說map、filter、reduce等方法

3、深拷貝

一、什麼是深拷貝

深拷貝就是無論是基本數據類型仍是引用數據類型都從新拷貝一份, 不存在共用數據的現象code

二、實現

暴力版本 JSON.parse(JSON.stringify(object))

// lmran
let obj = JSON.parse(JSON.stringify($obj))
console.log(obj)             // 不能解決循環引用
/*
    VM348:1 Uncaught TypeError: Converting circular structure to JSON
    at JSON.stringify (<anonymous>)
    at <anonymous>:1:17
*/
delete $obj.c.d
let obj = JSON.parse(JSON.stringify($obj))
console.log(obj)             // 丟失了大部分屬性
/*
    {
        a: null
        c: {a: 1}
        date: "2020-04-05T09:51:32.610Z"
        e: {}
        f: {}
  }
*/

存在的問題:regexp

一、會忽略 undefined對象

二、會忽略 symbol

三、不能序列化函數

四、不能解決循環引用的對象

五、不能正確處理new Date()

六、不能處理正則

七、不能處理new Error()

初版 基礎版本

遞歸遍歷對象屬性

// lmran
function deepCopy (obj) {
    if (obj === null || typeof obj !== 'object') {
        return obj
    }
    
    let copy = Array.isArray(obj) ? [] : {}
    Object.keys(obj).forEach(v => {
        copy[key] = deepCopy(obj[key])
    })

    return copy
}

deepCopy($obj)
/*
VM601:23 Uncaught RangeError: Maximum call stack size exceeded
    at <anonymous>:23:30
    at Array.forEach (<anonymous>)
    at deepCopy (<anonymous>:23:22)
*/
delete $obj.c.d
deepCopy($obj)
/*
{
    a: null
    b: undefined
    c: {a: 1}
    date: {}
    e: {}
    f: {}
    func: ƒ ()
    symbol: Symbol()
}
*/

存在的問題是:

一、不能解決循環引用的對象

二、不能正確處理new Date()

三、不能處理正則

四、不能處理new Error()

第二版 解決循環引用

先解決解決循環遍歷問題, 解決辦法是將對象,對象屬性存儲在數組中查看下次遍歷時有無已經遍歷過的對象,有則直接返回, 不然繼續遍歷

// lmran
function deepCopy (obj, cache = []) {
    if (obj === null || typeof obj !== 'object') {
        return obj
    }

    const item = cache.filter(item => item.original === obj)[0]
    if (item) return item.copy
    
    let copy = Array.isArray(obj) ? [] : {}
    cache.push({
        original: obj,
        copy
    })

    Object.keys(obj).forEach(key => {
        copy[key] = deepCopy(obj[key], cache)
    })

    return copy
}
deepCopy($obj)
/*
{
    a: null
    b: undefined
    c: {a: 1, d: {…}}
    date: {}
    e: {}
    f: {}
    func: ƒ ()
    symbol: Symbol()
}

完美解決了循環引用問題, 可是仍然存在幾個小問題,都屬於同一類問題

第三版 解決特殊值

對於最終的幾個對象的處理,能夠判斷類型, 從新new一個返回就能夠了

// lmran
function deepCopy (obj, cache = []) {
    if (obj === null || typeof obj !== 'object') {
        return obj
    }

    if (Object.prototype.toString.call(obj) === '[object Date]') return new Date(obj)
    if (Object.prototype.toString.call(obj) === '[object RegExp]') return new RegExp(obj)
    if (Object.prototype.toString.call(obj) === '[object Error]') return new Error(obj)
    const item = cache.filter(item => item.original === obj)[0]
    if (item) return item.copy
    
    let copy = Array.isArray(obj) ? [] : {}
    cache.push({
        original: obj,
        copy
    })

    Object.keys(obj).forEach(key => {
        copy[key] = deepCopy(obj[key], cache)
    })

    return copy
}
deepCopy($obj)
/*
{
    a: null
    b: undefined
    c: {a: 1, d: {…}}
    date: Fri Apr 10 2020 20:06:08 GMT+0800 (中國標準時間) {}
    e: /regexp/
    f: Error: Error: error at deepCopy (<anonymous>:8:74) at <anonymous>:19:21 at Array.forEach (<anonymous>) at deepCopy (<anonymous>:18:22) at <anonymous>:24:1
    func: ƒ ()
    symbol: Symbol()
}
*/

第四版 解決函數引用相同

到這裏基本功能彷佛已經實現, 可是還存在一個問題, 就是函數引用了同一個內存地址, 對於這個問題網上大部分都是直接返回或者返回爲對象, 包括lodash也是這麼處理的

const isFunc = typeof value == 'function'
 if (isFunc || !cloneableTags[tag]) {
        return object ? value : {}
 }

那麼如何結局這個問題就須要用eval函數, 雖然說這個函數已經不推薦使用, 可是它仍是能解決問題的。 函數這裏分兩種: 普通函數箭頭函數, 區分這二者只須要看有無prototype, 有prototype屬性就屬於普通函數, 沒有就是箭頭函數

// lmran
function copyFunction(func) {
    let fnStr = func.toString()
    return func.prototype ? eval(`(${fnStr})`) : eval(fnStr)
}

function deepCopy (obj, cache = []) {
    if (typeof obj === 'function') {
        return copyFunction(obj)
    }
    if (obj === null || typeof obj !== 'object') {
        return obj
    }

    if (Object.prototype.toString.call(obj) === '[object Date]') return new Date(obj)
    if (Object.prototype.toString.call(obj) === '[object RegExp]') return new RegExp(obj)
    if (Object.prototype.toString.call(obj) === '[object Error]') return new Error(obj)
   
    const item = cache.filter(item => item.original === obj)[0]
    if (item) return item.copy
    
    let copy = Array.isArray(obj) ? [] : {}
    cache.push({
        original: obj,
        copy
    })

    Object.keys(obj).forEach(key => {
        copy[key] = deepCopy(obj[key], cache)
    })

    return copy
}
deepCopy($obj).func === $obj.func // false

## 總結

至此, 深淺拷貝已所有實現,但學無止境,咱們還能夠考慮使用 Proxy提高來深拷貝性能,經過攔截 setget 實現,固然 Object.defineProperty() 也能夠。感興趣的同窗能夠查看這文章,文章介紹的很詳細頭條面試官:你知道如何實現高性能版本的深拷貝嘛?

針對@Brota掘友提出的對於函數引用相同的問題, 文中現已修復

文章中有什麼問題, 歡迎你們積極指出, 不勝感激!!!

參考

相關文章
相關標籤/搜索