你最少用幾行代碼實現深拷貝?

前言

深度克隆(深拷貝)一直都是初、中級前端面試中經常被問到的題目,網上介紹的實現方式也都各有千秋,大體可以概括爲三種方式:

  1. JSON.stringify+JSON.parse, 這個很好理解;

  2. 全量判斷類型,根據類型做不同的處理

  3. 2 的變型,簡化類型判斷過程

前兩種比較常見也比較基礎,所以我們今天主要討論的是第三種。

閱讀全文你將學習到:

  1. 更簡潔的深度克隆方式

  2. Object.getOwnPropertyDescriptors()api

  3. 類型判斷的通用方法

問題分析

深拷貝 自然是 相對 淺拷貝 而言的。我們都知道 引用數據類型 變量存儲的是數據的引用,就是一個指向內存空間的指針, 所以如果我們像賦值簡單數據類型那樣的方式賦值的話,其實只能複製一個指針引用,並沒有實現真正的數據克隆。

通過這個例子很容易就能理解:

const obj1 = {
    name: 'superman'
}
const obj2 = obj1;
obj1.name = '前端切圖仔';
console.log(obj2.name); // 前端切圖仔
複製代碼

所以深度克隆就是爲了解決引用數據類型不能被通過賦值的方式 複製 的問題。

引用數據類型

我們不妨來羅列一下引用數據類型都有哪些:

  • ES6 之前:Object, Array, Date, RegExp, Error,

  • ES6 之後:Map, Set, WeakMap, WeakSet,

所以,我們要深度克隆,就需要對數據進行遍歷並根據類型採取相應的克隆方式。當然因爲數據會存在多層嵌套的情況,採用遞歸是不錯的選擇。

簡單粗暴版本

function deepClone(obj) {
    let res = {};
    // 類型判斷的通用方法
    function getType(obj) {
        return Object.prototype.toString.call(obj).replaceAll(new RegExp(/\[|\]|object /g)"");
    }
    const type = getType(obj);
    const reference = ["Set""WeakSet""Map""WeakMap""RegExp""Date""Error"];
    if (type === "Object") {
        for (const key in obj) {
            if (Object.hasOwnProperty.call(obj, key)) {
                res[key] = deepClone(obj[key]);
            }
        }
    } else if (type === "Array") {
        console.log('array obj', obj);
        obj.forEach((e, i) ={
            res[i] = deepClone(e);
        });
    }
    else if (type === "Date") {
        res = new Date(obj);
    } else if (type === "RegExp") {
        res = new RegExp(obj);
    } else if (type === "Map") {
        res = new Map(obj);
    } else if (type === "Set") {
        res = new Set(obj);
    } else if (type === "WeakMap") {
        res = new WeakMap(obj);
    } else if (type === "WeakSet") {
        res = new WeakSet(obj);
    }else if (type === "Error") {
        res = new Error(obj);
    }
     else {
        res = obj;
    }
    return res;
}
複製代碼

其實這就是我們最前面提到的第二種方式,很傻對不對,明眼人一眼就能看出來有很多冗餘代碼可以合併。

我們先進行最基本的優化:

合併冗餘代碼

將一眼就能看出來冗餘的代碼合併下。

function deepClone(obj) {
    let res = null;
    // 類型判斷的通用方法
    function getType(obj) {
        return Object.prototype.toString.call(obj).replaceAll(new RegExp(/\[|\]|object /g)"");
    }
    const type = getType(obj);
    const reference = ["Set""WeakSet""Map""WeakMap""RegExp""Date""Error"];
    if (type === "Object") {
        res = {};
        for (const key in obj) {
            if (Object.hasOwnProperty.call(obj, key)) {
                res[key] = deepClone(obj[key]);
            }
        }
    } else if (type === "Array") {
        console.log('array obj', obj);
        res = [];
        obj.forEach((e, i) ={
            res[i] = deepClone(e);
        });
    }
    // 優化此部分冗餘判斷
    // else if (type === "Date") {
    //     res = new Date(obj);
    // } else if (type === "RegExp") {
    //     res = new RegExp(obj);
    // } else if (type === "Map") {
    //     res = new Map(obj);
    // } else if (type === "Set") {
    //     res = new Set(obj);
    // } else if (type === "WeakMap") {
    //     res = new WeakMap(obj);
    // } else if (type === "WeakSet") {
    //     res = new WeakSet(obj);
    // }else if (type === "Error") {
    //   res = new Error(obj);
    //}
    else if (reference.includes(type)) {
        res = new obj.constructor(obj);
    } else {
        res = obj;
    }
    return res;
}
複製代碼

爲了驗證代碼的正確性,我們用下面這個數據驗證下:

const map = new Map();
map.set("key""value");
map.set("ConardLi""coder");

const set = new Set();
set.add("ConardLi");
set.add("coder");

const target = {
    field1: 1,
    field2: undefined,
    field3: {
        child: "child",
    },
    field4: [2, 4, 8],
    empty: null,
    map,
    set,
    bool: new Boolean(true),
    num: new Number(2),
    str: new String(2),
    symbol: Object(Symbol(1)),
    date: new Date(),
    reg: /\d+/,
    error: new Error(),
    func1: () ={
        let t = 0;
        console.log("coder", t++);
    },
    func2: function (a, b) {
        return a + b;
    },
};
//測試代碼
const test1 = deepClone(target);
target.field4.push(9);
console.log('test1: ', test1);
複製代碼

執行結果:

還有進一步優化的空間嗎?

答案當然是肯定的。

// 判斷類型的方法移到外部,避免遞歸過程中多次執行
const judgeType = origin ={
    return Object.prototype.toString.call(origin).replaceAll(new RegExp(/\[|\]|object /g)"");
};
const reference = ["Set""WeakSet""Map""WeakMap""RegExp""Date""Error"];
function deepClone(obj) {
    // 定義新的對象,最後返回
     //通過 obj 的原型創建對象
    const cloneObj = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));

    // 遍歷對象,克隆屬性
    for (let key of Reflect.ownKeys(obj)) {
        const val = obj[key];
        const type = judgeType(val);
        if (reference.includes(type)) {
            newObj[key] = new val.constructor(val);
        } else if (typeof val === "object" && val !== null) {
            // 遞歸克隆
            newObj[key] = deepClone(val);
        } else {
            // 基本數據類型和function
            newObj[key] = val;
        }
    }
    return newObj;
}
複製代碼

執行結果如下:

  • Object.getOwnPropertyDescriptors() 方法用來獲取一個對象的所有自身屬性的描述符。

  • 返回所指定對象的所有自身屬性的描述符,如果沒有任何自身屬性,則返回空對象。

具體解釋和內容見 MDN

這樣做的好處就是能夠提前定義好最後返回的數據類型。

這個實現參考了網上一位大佬的實現方式,個人覺得理解成本有點高,而且對數組類型的處理也不是特別優雅, 返回類數組。

我在我上面代碼的基礎上進行了改造,改造後的代碼如下:

function deepClone(obj) {
    let res = null;
    const reference = [Date, RegExp, Set, WeakSet, Map, WeakMap, Error];
    if (reference.includes(obj?.constructor)) {
        res = new obj.constructor(obj);
    } else if (Array.isArray(obj)) {
        res = [];
        obj.forEach((e, i) ={
            res[i] = deepClone(e);
        });
    } else if (typeof obj === "Object" && obj !== null) {
        res = {};
        for (const key in obj) {
            if (Object.hasOwnProperty.call(obj, key)) {
                res[key] = deepClone(obj[key]);
            }
        }
    } else {
        res = obj;
    }
    return res;
}
複製代碼

雖然代碼量上沒有什麼優勢,但是整體的理解成本和你清晰度上我覺得會更好一點。那麼你覺得呢?

最後,還有循環引用問題,避免出現無線循環的問題。

我們用 hash 來存儲已經加載過的對象,如果已經存在的對象,就直接返回。

function deepClone(obj, hash = new WeakMap()) {
    if (hash.has(obj)) {
        return obj;
    }
    let res = null;
    const reference = [Date, RegExp, Set, WeakSet, Map, WeakMap, Error];

    if (reference.includes(obj?.constructor)) {
        res = new obj.constructor(obj);
    } else if (Array.isArray(obj)) {
        res = [];
        obj.forEach((e, i) ={
            res[i] = deepClone(e);
        });
    } else if (typeof obj === "Object" && obj !== null) {
        res = {};
        for (const key in obj) {
            if (Object.hasOwnProperty.call(obj, key)) {
                res[key] = deepClone(obj[key]);
            }
        }
    } else {
        res = obj;
    }
    hash.set(obj, res);
    return res;
}
複製代碼

總結

對於深拷貝的實現,可能存在很多不同的實現方式,關鍵在於理解其原理,並能夠記住一種最容易理解和實現的方式,面對類似的問題才能做到 臨危不亂,泰然自若。上面的實現你覺得哪個更好呢?歡迎大佬們在評論區交流~

更文不易, 看完記得點個贊支持一下哦~ 這將是我寫作的動力源泉~

作者:前端 superman

鏈接:‍https://juejin.cn/post/7075351322014253064

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/KYW2UoJUVqGB2OCSF9vBFg