「前端基礎」深拷貝

1. 背景:爲何有必要掌握深拷貝?

在業務中常常會遇到須要對模板數據/初始數據進行加工處理,並且,該數據可能能須要在多處被複用。面試

簡單粗暴更改原始數據的作法,會污染其餘依賴項,因此咱們須要對原始數據進行一份深拷貝,保持各個依賴項數據的獨立性,作到既能夠複用,又不互相污染。segmentfault

2. JS中沒有自帶的深拷貝API

也許咱們用到過Object.assign()進行對象合併,亦或者用數組的slice(), concat()方法對數組進行復制,但這幾種方法都不是深拷貝,是淺拷貝。數組

簡言之,深拷貝就是每一個對象都是獨立的,獨立意味着擁有各自的獨立內存空間;而淺拷貝意味着擁有公共的引用空間。函數

因此,值類型的數據不存在淺拷貝的問題,只有引用數據類型才存在。願更深刻的瞭解深拷貝和淺拷貝的,能夠參考文章最末尾的「參考文章」。post

3. 對於數組的深拷貝,一個很簡單的作法:

let recursiveClone = val => Array.isArray(val) ? Array.from(val, recursiveClone) : val;

4. 封裝:對於任意一個JS數據類型的深拷貝

概述JS的數據類型,從大體上分爲兩種:prototype

  1. 基本數據類型:string, number, boolean, undefined, null, symbol
  2. 引用數據類型: object(這是一個統稱,包含object, array, function 以及js單體內置對象如Date, RegExp, Error等)

對於基本數據類型,不存在深拷貝的問題,由於它們是值類型的數據。值類型的數據存放在棧內存中,從新賦值就是獨立的。code

而對於衆多的引用數據類型,須要分別進行處理,集中處理object和array,這也是咱們在業務中遇到最多的狀況。對象

5.上代碼:

const deepCloneTypes = ['Object', 'Array', 'Map', 'Set'];

function isObject(source) {
    const type = typeof source;
    return source !== null && (type === 'object' || type === 'function')
}

function getType(source) {
    return (Object.prototype.toString.call(source)).split(' ')[1].slice(0, -1)
}

function processOtherType(source) {
    const Ctor = source.constructor;
    return new Ctor(source);
}

function processFunctionType (source) {
    let _source = source.toString();
    // 區分是不是箭頭函數
    if (source.prototype) {
        // 若是有prototype就是普通函數
        let argsReg = /function\s*\w*\(([^\)]*)\)/;
        let bodyReg = /\{([\s\S]*)\}/;
        let fnArgs = (argsReg.exec(source))[1];
        let fnBody = (bodyReg.exec(source))[1];
        console.log(fnArgs, fnBody);
        return new Function (fnArgs, fnBody)
    } else {
        // 箭頭函數沒有prototype
        return eval(_source)
    }
}

function deepClone (source, map = new WeakMap()) {
    // 首先用typeof來篩選是不是引用數據類型,若是連引用數據類型都不是,那麼就判斷是基本數據類型,直接返回便可
    if (!isObject(source)) {
        return source
    }

    const type = getType(source);
    let cloneTarget;

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

    // 接下來判斷是不是須要進行循環拷貝的引用數據類型,諸如new Boolean, new Number這樣的,也不須要循環拷貝
    if (!deepCloneTypes.includes(type)) {
        cloneTarget = processOtherType(source);
    } else {
        cloneTarget = new source.constructor();
        // return Object.create(source.constructor.prototype) //不能這樣,這樣是創造了一個對象

        if (type === 'Object' || type === 'Array') {
            let keys = type === 'Object' ? Object.keys(source) : undefined; // 若是支持optional chaining的話能夠寫成?.
            (keys || source).forEach((val, key) => {
                if (keys) {
                    key = val;
                }
                cloneTarget[key] = deepClone(source[key], map); //在這裏進行遞歸調用
            })
        }
    
        if (type === 'Function') {
            cloneTarget = processFunctionType(source)
        }

        if (type === 'Map') {
            source.forEach((val, key) => {
                cloneTarget.set(key, deepClone(val, map))
            })
        }

        if (type === 'Set') {
            source.forEach((val, key) => {
                cloneTarget.add(deepClone(val, map))
            })
        }
    }

    return cloneTarget
}

參考:遞歸

如何寫出一個驚豔面試官的深拷貝?ip

JavaScript 深刻了解基本類型和引用類型的值

Array.from() 五個超好用的用途

相關文章
相關標籤/搜索