iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 11
0
Modern Web

從技術文章深入學習 JavaScript系列 第 17

Day 17 [深拷貝] 如何寫出一個驚艷面試官的深拷貝

  • 分享至 

  • xImage
  •  

文章選自

作者:ConardLi

連接:https://juejin.im/post/6844903929705136141

來源:掘金

乞丐版

利用:

JSON.parse(JSON.stringify());
let obj = { 
  foo: 'foo'
}
let obj2 = JSON.parse(JSON.stringify(obj));
obj2.foo = 'bar'
console.log(obj);
console.log(obj2);

https://ithelp.ithome.com.tw/upload/images/20201001/20124350QSYcvp2hYG.png

優點:

  1. 簡單,且可以應付,且可以應付很多應用場景

缺點 :

  1. 可能無法拷貝其他引用類型
  2. 無法拷貝函數
  3. 循環引用問題

基礎版本

先寫一個簡單的淺拷貝

function shallowCopy(obj) {
  let targetObj = {} 
  for (const key in obj) {
    targetObj[key] = obj[key]
  }
  return targetObj
}

我們可以透過遞歸(因為不知道對象總共有幾層)將上面的淺拷貝改寫成深拷貝

  1. 如果是原始類型,無需繼續拷貝,直接返回
  2. 如果是引用類型,創建一個新的對象,遍歷需要克隆的對象,將需要克隆對象的屬性執行深拷貝後依次添加到新對像上。
function deepCopy(target) {
  if (typeof target === 'object') {
    let cloneTarget = {};
    for (const key in target) {
      // 底下透過遞歸會一直往對象深層探勘
      cloneTarget[key] = deepCopy(target[key]);
    }
    return cloneTarget;
  } else {
    return target;
  }
};

測試

const school = {
  classA: {
    stuA: {
      name: 'Jack',
      age: 15
    },
    stuB: {
      name: 'Bill',
    }
  },
  principal: 'Mike',
  area: 22500,
  isSchool: true
}

deepCopy(school)

https://ithelp.ithome.com.tw/upload/images/20201001/20124350HZ7b97Dzy9.png

可以說是一個最基本款的深拷貝,但是有一個問題是我們有考慮慮,如果target是數組呢?

解決基礎版數組問題

只要在一開始定義的時候,多一個是不是數組的判斷即可

function deepCopy(target) {
  if (typeof target === 'object') {
    // let cloneTarget = {};
    // 修改後的
    // 判斷target是不是數組,是的話cloneTarget為[]不是的話為{}
    let cloneTarget = Array.isArray(target) ? [] : {}
    for (const key in target) {
      // 底下透過遞歸會一直往對象深層探勘
      cloneTarget[key] = deepCopy(target[key]);
    }
    return cloneTarget;
  } else {
    return target;
  }
};

測試

const school = {
  classA: {
    stuA: {
      name: 'Jack',
      age: 15
    },
    stuB: {
      name: 'Bill',
      hobbies: ['sleep', 'sing', 'basketball']
    }
  },
  principal: 'Mike',
  area: 22500,
  isSchool: true
}

console.log(deepCopy(school))

https://ithelp.ithome.com.tw/upload/images/20201001/20124350Mvwi3AYxWa.png

循環引用

先來看這個例子

const school = {
  classA: {
    stuA: {
      name: 'Jack',
      age: 15
    },
    stuB: {
      name: 'Bill',
      hobbies: ['sleep', 'sing', 'basketball']
    }
  },
  principal: 'Mike',
  area: 22500,
  isSchool: true
}
school.foo = school
deepCopy(school)

https://ithelp.ithome.com.tw/upload/images/20201001/20124350TMDsNxJPA7.png

為何會這樣?

這個主要原因是調用棧溢出(循環引用本身沒問題)

至於甚麼是調用棧溢出呢?可以參考底下這個問答串

https://stackoverflow.com/questions/6095530/maximum-call-stack-size-exceeded-error

解決方法

創造一個儲存空間,用來存儲當前對象和拷貝對象的對應關係,當需要拷貝當前對象時,先去存儲空間中找,有沒有拷貝過這個對象,如果有的話( 代表有循環引用的情況發生 )直接返回,如果沒有的話繼續拷貝,這樣就巧妙化解的循環引用的問題。

function deepCopy(target, map = new Map()) {
  if (typeof target === 'object') {
    let cloneTarget = Array.isArray(target) ? [] : {};

    for (const key in target) {
      // Map會持續記錄拷貝過的對象(這裡傳入之前的map,因此不會默認再new Map)
      cloneTarget[key] = deepCopy(target[key], map);

      // 底下這幾行是為了預防循環引用這種情況發生
      // 只要曾經拷貝過的對象都會被記錄
      // 比方說這個例子,當我們遍歷到 
      // cloneTarget['foo'] = deepCopy(school['foo'], map);
      // 因為school.foo引用的是自身(即是school),所以當我們調用deepCopy(school['foo'], map)		  的時候,map.get(target)可以查找的到(cloneTarget自身)
      // 因此代表的意義是
      // cloneTarget['foo'] = map.get(school)
      if (map.get(target)) {
        return map.get(target);
      }
      map.set(target, cloneTarget);
      console.log(cloneTarget);
    }
    return cloneTarget;
  } else {
    return target;
  }
};
school = {
  name: {
    age: 19
  }
}
school.foo = school

console.log(deepCopy(school));

更好的 WeakMap

先來看看甚麼是WeakMap

MDN官方解釋:

https://ithelp.ithome.com.tw/upload/images/20201001/20124350cZqIKfPd2w.png

強引用弱引用?

詳情可以查看此篇文章

https://juejin.im/post/6844904169417998349#heading-5

function deepCopy(target, map = new WeakMap()) {
    ...
}

其他數據類型

上面我們僅考慮array以及普通的object,尚有其他引用類型需要我們考慮

判斷引用類型

除了引用類型還須考慮到這兩個範疇

// 返回的布林值代表是否為引用類型
function isObject(target) {
    const type = typeof target;
    return target !== null && (type === 'object' || type === 'function');
}

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

獲取數據類型

透過 Object.prototype.toString.call(target) 獲取較精準地引用類型

這裡封裝了一個判斷引用類型種類的函數

function getType(target) {
    return Object.prototype.toString.call(target);
}
// 並將較常見的類型抓出來方便之後使用
const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';

// 這裡會需要列出numberTag主要是因為
// let numObj = new Number(123)
// numObj的類型會是對象
const boolTag = '[object Boolean]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
const regexpTag = '[object RegExp]';
const symbolTag = '[object Symbol]';


這兩將分成兩大類來看

  • 可以直接返回的類型
  • 可以繼續克隆下去的類型

可繼續遍歷的類型

這裡只考慮四種類型 (還有更多可以遍歷的類型)

  1. Array
  2. Object
  3. Map
  4. Set

主要是改進這行,藉由 constructor

比方說創建一個對象除了可以

let obj = {}
// 也可以這樣
let obj = new Object()
// 之前初始化數據
let cloneTarget = Array.isArray(target) ? [] : {};

// 這裡封裝一個函數可以透過constructor初始化
function getInit(target) {
    const Ctor = target.constructor;
    return new Ctor();
}

目前拷貝函數長怎樣

function clone(target, map = new WeakMap()) {
    // 克隆原始类型
    if (!isObject(target)) {
        return target;
    }

    // 初始化
    const type = getType(target);
    let cloneTarget;
    if (deepTag.includes(type)) {
        cloneTarget = getInit(target, type);
    }

    // 防止循环引用
    if (map.get(target)) {
        return map.get(target);
    }
    map.set(target, cloneTarget);

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

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

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

    return cloneTarget;
}

不可繼續遍歷的類型

先處理可以透過構造函數以及數據創建的類型

const boolTag = '[object Boolean]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
// 封裝一個函數處理這些
function cloneOtherType(target, type) {
    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以及正則的拷貝函數

// Symbol
function cloneSymbol(targe) {
    return Object(Symbol.prototype.valueOf.call(targe));
}

// 正则:

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

函數類型

基本上兩個對象引用同一個內存地址的函數其實沒啥關係

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

但還是會希望可以再繼續克隆出一個新的函數,不然直接返回有點單調

思路:

  1. 透過prototype區分是否為箭頭函數(箭頭函數沒有)
  2. 如果是箭頭函數,則透過 eval 和函數字符串來重新生成一個箭頭函數
  3. 如果是普通函數則透過正則處理
function cloneFunction(func) {
    const bodyReg = /(?<={)(.|\n)+(?=})/m;
    const paramReg = /(?<=\().+(?=\)\s+{)/;
    const funcString = func.toString();
    if (func.prototype) {
        console.log('普通函数');
        const param = paramReg.exec(funcString);
        const body = bodyReg.exec(funcString);
        if (body) {
            console.log('匹配到函数体:', body[0]);
            if (param) {
                const paramArr = param[0].split(',');
                console.log('匹配到参数:', paramArr);
                return new Function(...paramArr, body[0]);
            } else {
                return new Function(body[0]);
            }
        } else {
            return null;
        }
    } else {
        return eval(funcString);
    }
}


上一篇
Day 16 [淺拷貝] 淺拷貝與深拷貝
下一篇
Day 18 [123] [前端漫談_1] 從 for of 聊到 Generator
系列文
從技術文章深入學習 JavaScript29
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言