基礎鞏固-你最少用幾行程式碼實現深拷貝?

語言: CN / TW / HK

點選上方  前端Q ,關注公眾號

回覆 加群 ,加入前端Q技術交流群

前言

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

  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);
複製程式碼

執行結果:

image.png

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

答案當然是肯定的。

// 判斷型別的方法移到外部,避免遞迴過程中多次執行
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;
}
複製程式碼

執行結果如下:

image.png
  • Object.getOwnPropertyDescriptors() 方法用來獲取一個物件的所有自身屬性的描述符。
  • 返回所指定物件的所有自身屬性的描述符,如果沒有任何自身屬性,則返回空物件。

具體解釋和內容見 MDN [1]

這樣做的好處就是能夠提前定義好最後返回的資料型別。

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

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

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]);
            }
        }
        hash.set(obj, res);
    } else {
        res = obj;
    }
    return res;
}
複製程式碼

總結

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

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

關於本文

作者:前端superman

https://juejin.cn/post/7075351322014253064

往期推薦

最後

  • 歡迎加我微信,拉你進技術群,長期交流學習...

  • 歡迎關注「前端Q」,認真學前端,做個專業的技術人...

點個 在看 支援我吧