JavaScript對象深拷貝/淺拷貝遇到的坑和解決方法

若是本文對您有任何幫助或者您有任何想要提出的意見或問題,請在本文下方回覆,誠摯歡迎各位參與討論,望各位不吝指教。
原載本身的小博客 JavaScript對象拷貝遇到的坑和解決方法 | 手柄君的小閣,因此無恥地算原創吧

近期參與某集訓,JavaScript,遇到一對象拷貝問題,獲得需求:
給一個對象,請編寫一個函數,使其能夠拷貝一個對象,返回這個拷貝獲得的新對象:
舉例以下:html

function clone(obj){
    //DO SOMETHING
    return newObject; //返回拷貝獲得的新對象
}

首先想到解法以下:數組

> ES6解構賦值(淺拷貝):

function clone(obj){
    return {...obj};
}

獲得新對象爲原始對象淺拷貝,即屬性Key一致,值若是是數或者字符串則值傳遞,不然爲地址傳遞,即Value引用和源對象一致,可根據下方運行測試:函數

var a = {a:1, b:2, c:3, d:[0, 1, 2]}
var b = clone(a);
console.log(b.d[1]); //1
b.d[1] = 2;
console.log(b.d[1]); //2
console.log(a.d[1]); //2

對複製後的對象中包含的數組或者對象進行編輯,影響了源對象,這顯然不是咱們想要的結果,可是在對象內不包含數組或對象時,該方法不失爲一個快速建立對象拷貝的實用方法。
在ES6中,Object提供了一個 assign() 方法,也能夠實現相同效果測試

> ES6 Object.assign()(淺拷貝):

function clone(obj){
    return Object.assign({},obj);
}

運行效果和前一種方式基本一致,根據MDN描述,Object.assign() 方法用於將全部可枚舉屬性的值從一個或多個源對象複製到目標對象,容許至少兩個參數,第一個參數爲拷貝的目標對象,在方法執行結束後會被返回,其他參數將做爲拷貝來源。
前面兩種方法均爲淺拷貝,那麼對於對象內包含對象或數組的對象,咱們該怎樣拷貝呢?
咱們的老師提供了一種方法以下,缺陷稍後再談.net

> For...in遍歷並遞歸(深拷貝):

function clone(obj) {
    var newobj = obj.constructor === Array ? [] : {};
    if (typeof obj !== "object") {
        return obj;
    } else {
        for (var i in obj) {
            newobj[i] = typeof obj[i] === "object" ? clone(obj[i]) : obj[i];
        }
    }
    return newobj;
}

一樣使用前文中的測試數據:code

var a = {a:1, b:2, c:3, d:[0, 1, 2]}
var b = clone(a);
console.log(b.d[1]); //1
b.d[1] = 2;
console.log(b.d[1]); //2
console.log(a.d[1]); //1

可見該方法能夠正確地對對象進行深拷貝,並根據參數類型爲數組或對象進行進行判斷並分別處理,可是該方法有必定缺陷:htm

1,在存在Symbol類型屬性key時,沒法正確拷貝,能夠嘗試如下測試數據:對象

var sym = Symbol();
var a = {a:1, b:2, c:3, d:[0, 1, 2], [sym]:"symValue"}
var b = clone(a);
b.d[1] = 2;
console.log(b.d[1]); //2
console.log(a.d[1]); //1
console.log(a[sym]); //"symValue"
console.log(b[sym]); //undefined

能夠發現拷貝獲得的對象b,不存在Symbol類型對象爲屬性名的屬性。
那麼能夠發現,問題主要出在For...in遍歷屬性沒法得到Symbol類型Key致使,那麼有什麼方法能夠遍歷到這些呢?
在ES6中Reflect包含的靜態方法ownKeys() 能夠獲取到這些key,根據MDN描述,這個方法獲取到的返回值等同於Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))。遞歸

那麼使用ES6解構賦值和Reflect.ownKeys() 組合使用,改寫上文函數,獲得:ip

> ES6解構賦值 & Reflect.ownKeys() 遍歷並遞歸(深拷貝):

function clone(obj) {
    var newobj = obj.constructor === Array ? [...obj] : {...obj};
    if (typeof obj !== "object") {
        return obj;
    } else {
        Reflect.ownKeys(newobj).forEach(i => {
            newobj[i] = typeof obj[i] === "object" ? clone(obj[i]) : obj[i];
        });
    }
    return newobj;
}

運行相同的測試語句:

var sym = Symbol();
var a = {a:1, b:2, c:3, d:[0, 1, 2], [sym]:"symValue"}
var b = clone(a);
b.d[1] = 2;
console.log(b.d[1]); //2
console.log(a.d[1]); //1
console.log(a[sym]); //"symValue"
console.log(b[sym]); //"symValue"
b[sym] = "newValue";
console.log(a[sym]); //"symValue"
console.log(b[sym]); //"newValue"

能夠發現Symbol類型的key也被正確拷貝並賦值了,可是該方法依然有必定問題,以下:

2,在對象內部存在環時,堆棧溢出,嘗試運行如下測試語句:

var a = { info: "a", arr: [0, 1, 2] };
var b = { data: a, info: "b", arr: [3, 4, 5] };
a.data = b;
var c = clone(a); //Error: Maximum call stack size exceeded. 報錯:堆棧溢出

解決這個的方法稍後再講,但目前來看已有的兩種深拷貝方法足夠平時使用,接下來正好提一下,ES5.1中包含的JSON對象,使用該對象亦可對對象進行深拷貝,會遇到的問題和第一種深拷貝方式同樣,沒法記錄Symbol爲屬性名的屬性,另外只能包含能用JSON字符串表示的數據類型,實現代碼以下:

> JSON對象轉義(深拷貝):

function clone(obj) {
    return JSON.parse(JSON.stringify(obj);
}

JSON.stringify() 首先將對象序列化爲字符串,再由JSON.parse() 反序列化爲對象,造成新的對象。
回到前面提到的問題2,若是對象內包含環,怎麼辦,個人實現思路爲使用兩個對象做爲相似HashMap,記錄源對象的結構,並在每層遍歷前檢查對象是否已經被拷貝過,若是是則從新指向到拷貝好的對象,防止無限遞歸。實現代碼以下(配有註釋):

> Map記錄並遞歸(深拷貝)

/**
 * 深拷貝(包括Symbol)
 * @param {Object} obj
 */
function clone(obj) {
    const map = {}; //空對象,記錄源對象
    const mapCopy = {}; //空對象,記錄拷貝對象
    /**
     * 在theThis對象中,查找e對象的key,若是找不到,返回false
     * @param {Object} e 要查找的對象
     * @param {Object} theThis 在該對象內查找
     * @returns {symbol | boolean}
     */
    function indexOfFun(e, theThis) {
        let re = false;
        for (const key of Reflect.ownKeys(theThis)) {
            if (e === theThis[key]) {
                re = key;
                break;
            }
        }
        return re;
    }
    /**
     * 在Map對象中,查找e對象的key
     * @param {Object} e 
     */
    const indexOfMap = e => indexOfFun(e, map);
    /**
     * 在Map中記錄obj對象內全部對象的地址
     * @param {Object} obj 要被記錄的對象
     */
    function bindMap(obj) {
        map[Symbol()] = obj;
        Reflect.ownKeys(obj).forEach(key => {
            //當屬性類型爲Object且還沒被記錄過
            if (typeof obj[key] === "object" && !indexOfMap(obj[key])) {
                bindMap(obj[key]); //記錄這個對象
            }
        });
    }
    bindMap(obj);
    /**
     * 拷貝對象
     * @param {Object} obj 要被拷貝的對象
     */
    function copyObj(obj) {
        let re;//用做返回
        if (Array.isArray(obj)) {
            re = [...obj]; //當obj爲數組
        } else {
            re = { ...obj }; //當obj爲對象
        }
        mapCopy[indexOfMap(obj)] = re; //記錄新對象的地址
        Reflect.ownKeys(re).forEach(key => { //遍歷新對象屬性
            if (typeof re[key] === "object") { //當屬性類型爲Object
                if (mapCopy[indexOfMap(re[key])]) { //當屬性已經被拷貝過
                    re[key] = mapCopy[indexOfMap(re[key])]; //修改屬性指向到先前拷貝好的對象
                } else {//當屬性尚未被拷貝
                    re[key] = copyObj(re[key]); //拷貝這個對象,並將屬性指向新對象
                }
            }
        });
        return re; //返回拷貝的新對象
    }
    return copyObj(obj); //執行拷貝並返回
}

運行前面的測試語句:

var a = { info: "a", arr: [0, 1, 2] };
var b = { data: a, info: "b", arr: [3, 4, 5] };
a.data = b;

var c = clone(a);
c.info = "c";
c.data.info = "d";
console.log(a.info); //"a"
console.log(a.data.info); //"b"
console.log(c.info); //"c"
console.log(c.data.info); //"d"

獲得該函數能夠正確地拷貝帶環對象。

在以上討論和研究結束後,同窗向我推薦了一個庫 lodash,測試了一下該庫存在 _.cloneDeep() 方法,實現深拷貝更爲完整和精緻,前文問題均沒有在該方法內被發現,在這裏提一波。


若是本文對您有任何幫助或者您有任何想要提出的意見或問題,請在本文下方回覆,誠摯歡迎各位參與討論,望各位不吝指教。
本文原載於https://www.bysb.net/3113.html
相關文章
相關標籤/搜索