leader:深拷貝有這5個段位,你只是青銅段位?還想漲薪?

Sunshine_Lin發表於2021-11-30

前言

大家好,我是林三心。前幾天跟leader在聊深拷貝

  • leader:你知道怎麼複製一個物件嗎?
  • 我:知道啊!不就深拷貝嗎?
  • leader:那你是怎麼深拷貝的?
  • 我:我直接一手JSON.parse(JSON.stringfy(obj))吃遍天
  • leader:兄弟,有空去看看lodash裡的deepClone,看看人家是怎麼實現的

哈哈,確實,深拷貝在日常開發中是有很多應用場景的,他也是非常重要的,寫一個合格的深拷貝方法是很有必要的。那怎麼才能寫一個合格的深拷貝方法呢?或者說,怎麼才能寫一個毫無破綻的深拷貝方法呢?

image.png

深拷貝 && 淺拷貝

我們們先來說說什麼是深拷貝,什麼是淺拷貝吧。

淺拷貝

所謂淺拷貝,就是隻複製最外一層,裡面的都還是相同引用

// 淺拷貝
const a = { name: 'sunshine_lin', age: 23, arr: [] }
const b = {}
for (let key in a){
    b[key] = a[key]
}

console.log(b) // { name: 'sunshine_lin', age: 23, arr: [] }
console.log(b === a) // false
console.log(b.arr === a.arr) // true

深拷貝

深拷貝,則是你將一個物件拷貝到另一個新變數,這個新變數指向的是一塊新的堆記憶體地址

// 深拷貝

function deepClone(target) {
    // ...實現深拷貝
}

const a = { name: 'sunshine_lin', age: 23, arr: [] }
const b = deepClone(a)

console.log(b) // { name: 'sunshine_lin', age: 23, arr: [] }
console.log(b === a) // false
console.log(b.arr === a.arr) // false

黃金版本

相信大多數人平時在實現深拷貝時,都會這麼去實現

function deepClone(target) {
    return JSON.parse(JSON.stringify(target))
}

const a = { name: 'sunshine_lin', age: 23 }
const b = deepClone(a)

console.log(b) // { name: 'sunshine_lin', age: 23 }
console.log(b === a) // false

雖然大多數時候這麼使用是沒問題的,但這種方式還是有很多缺點的

  • 1、物件中有欄位值為undefined,轉換後則會直接欄位消失
  • 2、物件如果有欄位值為RegExp物件,轉換後則欄位值會變成{}
  • 3、物件如果有欄位值為NaN、+-Infinity,轉換後則欄位值變成null
  • 4、物件如果有環引用,轉換直接報錯

截圖2021-10-02 下午9.34.22.png

鉑金版本

既然是要物件的深拷貝,那我可以建立一個空物件,並把需要拷貝的原物件的值一個一個複製過來就可以了呀!!!

function deepClone(target) {
    const temp = {}
    for (const key in target) {
        temp[key] = target[key]
    }
    return temp
}

const a = { name: 'sunshine_lin', age: 23 }
const b = deepClone(a)

console.log(b) // { name: 'sunshine_lin', age: 23 }
console.log(b === a) // false

但是其實上面這種做法是不完善的,因為我們們根本不知道我們們想拷貝的物件有多少層。。大家一聽到“不知道有多少層”,想必就會想到遞迴了吧,是的,使用遞迴就可以了。

function deepClone(target) {
    // 基本資料型別直接返回
    if (typeof target !== 'object') {
        return target
    }

    // 引用資料型別特殊處理
    const temp = {}
    for (const key in target) {
        // 遞迴
        temp[key] = deepClone(target[key])
    }
    return temp
}

const a = {
    name: 'sunshine_lin',
    age: 23,
    hobbies: { sports: '籃球',tv: '雍正王朝' }
}
const b = deepClone(a)

console.log(b)
// {
//     name: 'sunshine_lin',
//     age: 23,
//     hobbies: { sports: '籃球', tv: '雍正王朝' }
// }
console.log(b === a) // false

鑽石版本

前面我們們只考慮了物件的情況,但是沒把陣列情況也給考慮,所以我們們要加上陣列條件

function deepClone(target) {
    // 基本資料型別直接返回
    if (typeof target !== 'object') {
        return target
    }

    // 引用資料型別特殊處理
    
    // 判斷陣列還是物件
    const temp = Array.isArray(target) ? [] : {}
    for (const key in target) {
        // 遞迴
        temp[key] = deepClone(target[key])
    }
    return temp
}

const a = {
    name: 'sunshine_lin',
    age: 23,
    hobbies: { sports: '籃球', tv: '雍正王朝' },
    works: ['2020', '2021']
}
const b = deepClone(a)

console.log(b)
// {
//     name: 'sunshine_lin',
//     age: 23,
//     hobbies: { sports: '籃球', tv: '雍正王朝' },
//     works: ['2020', '2021']
// }
console.log(b === a) // false

星耀版本

前面實現的方法都沒有解決環引用的問題

  • JSON.parse(JSON.stringify(target))報錯TypeError: Converting circular structure to JSON,意思是無法處理環引用
  • 遞迴方法報錯Maximum call stack size exceeded,意思是遞迴不完,爆棧

截圖2021-10-02 下午10.06.58.png

// 環引用
const a = {}
a.key = a

那怎麼解決環引用呢?其實說難也不難,需要用到ES6的資料結構Map

  • 每次遍歷到有引用資料型別,就把他當做key放到Map中,對應的value是新建立的物件temp
  • 每次遍歷到有引用資料型別,就去Map中找找有沒有對應的key,如果有,就說明這個物件之前已經註冊過,現在又遇到第二次,那肯定就是環引用了,直接根據key獲取value,並返回value

截圖2021-10-02 下午10.18.19.png

function deepClone(target, map = new Map()) {
    // 基本資料型別直接返回
    if (typeof target !== 'object') {
        return target
    }

    // 引用資料型別特殊處理
    // 判斷陣列還是物件
    const temp = Array.isArray(target) ? [] : {}

+    if (map.get(target)) {
+        // 已存在則直接返回
+        return map.get(target)
+    }
+    // 不存在則第一次設定
+    map.set(target, temp)

    for (const key in target) {
        // 遞迴
        temp[key] = deepClone(target[key], map)
    }
    return temp
}

const a = {
    name: 'sunshine_lin',
    age: 23,
    hobbies: { sports: '籃球', tv: '雍正王朝' },
    works: ['2020', '2021']
}
a.key = a // 環引用
const b = deepClone(a)

console.log(b)
// {
//     name: 'sunshine_lin',
//     age: 23,
//     hobbies: { sports: '籃球', tv: '雍正王朝' },
//     works: [ '2020', '2021' ],
//     key: [Circular]
// }
console.log(b === a) // false

王者版本

image.png

剛剛我們們只是實現了

  • 基本資料型別的拷貝
  • 引用資料型別中的陣列,物件

但其實,引用資料型別可不止只有陣列和物件,我們還得解決以下的引用型別的拷貝問題,那怎麼判斷每個引用資料型別的各自型別呢?可以使用Object.prototype.toString.call()

型別toString結果
MapObject.prototype.toString.call(new Map())[object Map]
SetObject.prototype.toString.call(new Set())[object Set]
ArrayObject.prototype.toString.call([])[object Array]
ObjectObject.prototype.toString.call({})[object Object]
SymbolObject.prototype.toString.call(Symbol())[object Symbol]
RegExpObject.prototype.toString.call(new RegExp())[object RegExp]
FunctionObject.prototype.toString.call(function() {})[object Function]

我們先把以上的引用型別資料分為兩類

  • 可遍歷的資料型別
  • 不可遍歷的資料型別
// 可遍歷的型別
const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';

// 不可遍歷型別
const symbolTag = '[object Symbol]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';

// 將可遍歷型別存在一個陣列裡
const canForArr = ['[object Map]', '[object Set]',
                   '[object Array]', '[object Object]']

// 將不可遍歷型別存在一個陣列
const noForArr = ['[object Symbol]', '[object RegExp]', '[object Function]']

// 判斷型別的函式
function checkType(target) {
    return Object.prototype.toString.call(target)
}

// 判斷引用型別的temp
function checkTemp(target) {
    const c = target.constructor
    return new c()
}

可遍歷引用型別

主要處理以下四種型別

  • Map
  • Set
  • Object
  • Array

    function deepClone(target, map = new Map()) {
    
  • const type = checkType(target)

    // 基本資料型別直接返回

  • if (!canForArr.concat(noForArr).includes(type)) {
  • return target
  • }

    // 引用資料型別特殊處理

  • const temp = checkTemp(target)

    if (map.get(target)) {

      // 已存在則直接返回
      return map.get(target)

    }
    // 不存在則第一次設定
    map.set(target, temp)

    // 處理Map型別

  • if (type === mapTag) {
  • target.forEach((value, key) => {
  • temp.set(key, deepClone(value, map))
  • })
    +
  • return temp
  • }

    // 處理Set型別

  • if (type === setTag) {
  • target.forEach(value => {
  • temp.add(deepClone(value, map))
  • })
    +
  • return temp
  • }

    // 處理資料和物件
    for (const key in target) {

      // 遞迴
      temp[key] = deepClone(target[key], map)

    }
    return temp
    }

    const a = {
    name: 'sunshine_lin',
    age: 23,
    hobbies: { sports: '籃球', tv: '雍正王朝' },
    works: ['2020', '2021'],
    map: new Map([['haha', 111], ['xixi', 222]]),
    set: new Set([1, 2, 3]),
    }
    a.key = a // 環引用
    const b = deepClone(a)

    console.log(b)
    // {
    // name: 'sunshine_lin',
    // age: 23,
    // hobbies: { sports: '籃球', tv: '雍正王朝' },
    // works: [ '2020', '2021' ],
    // map: Map { 'haha' => 111, 'xixi' => 222 },
    // set: Set { 1, 2, 3 },
    // key: [Circular]
    // }
    console.log(b === a) // false

不可遍歷引用型別

主要處理以下幾種型別

  • Symbol
  • RegExp
  • Function

先把拷貝這三個型別的方法寫出來

// 拷貝Function的方法
function cloneFunction(func) {
    const bodyReg = /(?<={)(.|\n)+(?=})/m;
    const paramReg = /(?<=\().+(?=\)\s+{)/;
    const funcString = func.toString();
    if (func.prototype) {
        const param = paramReg.exec(funcString);
        const body = bodyReg.exec(funcString);
        if (body) {
            if (param) {
                const paramArr = param[0].split(',');
                return new Function(...paramArr, body[0]);
            } else {
                return new Function(body[0]);
            }
        } else {
            return null;
        }
    } else {
        return eval(funcString);
    }
}

// 拷貝Symbol的方法
function cloneSymbol(targe) {
    return Object(Symbol.prototype.valueOf.call(targe));
}

// 拷貝RegExp的方法
function cloneReg(targe) {
    const reFlags = /\w*$/;
    const result = new targe.constructor(targe.source, reFlags.exec(targe));
    result.lastIndex = targe.lastIndex;
    return result;
}

最終版本

image.png

function deepClone(target, map = new Map()) {

    // 獲取型別
    const type = checkType(target)


    // 基本資料型別直接返回
    if (!canForArr.concat(noForArr).includes(type)) return target


    // 判斷Function,RegExp,Symbol
  +  if (type === funcTag) return cloneFunction(target)
  +  if (type === regexpTag) return cloneReg(target)
  +  if (type === symbolTag) return cloneSymbol(target)

    // 引用資料型別特殊處理
    const temp = checkTemp(target)

    if (map.get(target)) {
        // 已存在則直接返回
        return map.get(target)
    }
    // 不存在則第一次設定
    map.set(target, temp)

    // 處理Map型別
    if (type === mapTag) {
        target.forEach((value, key) => {
            temp.set(key, deepClone(value, map))
        })

        return temp
    }

    // 處理Set型別
    if (type === setTag) {
        target.forEach(value => {
            temp.add(deepClone(value, map))
        })

        return temp
    }

    // 處理資料和物件
    for (const key in target) {
        // 遞迴
        temp[key] = deepClone(target[key], map)
    }
    return temp
}


const a = {
    name: 'sunshine_lin',
    age: 23,
    hobbies: { sports: '籃球', tv: '雍正王朝' },
    works: ['2020', '2021'],
    map: new Map([['haha', 111], ['xixi', 222]]),
    set: new Set([1, 2, 3]),
    func: (name, age) => `${name}今年${age}歲啦!!!`,
    sym: Symbol(123),
    reg: new RegExp(/haha/g),
}
a.key = a // 環引用

const b = deepClone(a)
console.log(b)
// {
//     name: 'sunshine_lin',
//     age: 23,
//     hobbies: { sports: '籃球', tv: '雍正王朝' },
//     works: [ '2020', '2021' ],
//     map: Map { 'haha' => 111, 'xixi' => 222 },
//     set: Set { 1, 2, 3 },
//     func: [Function],
//     sym: [Symbol: Symbol(123)],
//     reg: /haha/g,
//     key: [Circular]
// }
console.log(b === a) // false

結語

如果你覺得此文對你有一丁點幫助,點個贊,鼓勵一下林三心哈哈。或者可以加入我的摸魚群,我們一起好好學習啊啊啊啊啊啊啊,我會定時模擬面試,簡歷指導,答疑解惑,我們們互相學習共同進步!!
截圖2021-11-28 上午9.43.19.png

相關文章