JavaScript 淺拷貝和深拷貝

2023-01-16 15:01:43

JavaScript 中的拷貝分為兩種:淺拷貝和深拷貝。

一、淺拷貝

淺拷貝是指在拷貝過程中,只拷貝一個物件中的指標,而不拷貝實際的資料。所以,淺拷貝中修改新物件中的資料時,原物件中的資料也會被改變。

JavaScript 中淺拷貝可以通過如下幾種方式實現:

  • 使用結構賦值的方式,例如 let newObject = {...oldObject}
  • 使用 Object.assign() 方法,例如 let newObject = Object.assign({}, oldObject)

二、深拷貝

深拷貝是指在拷貝過程中,拷貝一個物件中的所有資料,並建立一個新物件,對新物件進行操作並不會影響到原物件。

1、常規場景

JavaScript 中深拷貝可以通過如下幾種方式實現:

  • 使用 JSON.parse(JSON.stringify(object)) 方法

需要注意的是:該方法會忽略 undefined 以及正規表示式型別的屬性。

const A = { a: 7788, b: undefined, c: new RegExp(/-/ig) },
    B = JSON.parse(JSON.stringify(A));

console.log('A', A);
console.log('B', B);

  • 使用遞迴的方式,手動拷貝物件的每一層
function deepCopy(obj) {
    if (typeof obj !== 'object' || obj === null) {
        return obj;
    }
    let copy;
    if (Array.isArray(obj)) {
        copy = [];
        for (let i = 0; i < obj.length; i++) {
            copy[i] = deepCopy(obj[i]);
        }
    } else {
        copy = {};
        for (let key in obj) {
            copy[key] = deepCopy(obj[key]);
        }
    }
    return copy;
}

const objA = { a: 123 },
    objB = { b: 456 };

// 淺拷貝
const objC = {...objA};
console.log('objA.a', objA.a);  // objA.a 123
console.log('objC.a', objA.a);  // objC.a 123
objC.a = 788;
console.log('objA.a', objA.a);  // objA.a 788
console.log('objC.a', objC.a);  // objC.a 788

// 深拷貝
const objD = deepCopy(objB);
console.log('objB.b', objB.b);  // objB.b 456
console.log('objD.b', objD.b);  // objD.b 456
objD.b = 899;
console.log('objB.b', objB.b);  // objB.b 456
console.log('objD.b', objD.b);  // objD.b 899

這個函數接受一個引數 obj,如果它不是物件或者是 null,那麼直接返回該引數。如果它是陣列,則建立一個新陣列並遞迴複製每一項。否則,建立一個新物件並遞迴複製每一個屬性。

  • 使用 lodash 類庫的_.cloneDeep函數、 underscore 中的 _.clone() 函數等第三方庫

2、特定場景一:內建物件型別的深拷貝

JavaScript 中複製內建物件型別(例如 Date,RegExp 等)的深拷貝可以使用特定的建構函式來重新建立該物件。

例如,對於 Date 物件,可以使用 new Date(originalDate.getTime()) 來建立一個新的日期物件,其中 originalDate.getTime() 返回原始日期物件的時間戳。

對於 RegExp 物件,可以使用 new RegExp(originalRegExp) 或 new RegExp(originalRegExp.source, originalRegExp.flags) 來建立一個新的正規表示式物件。

下面是一個使用建構函式來複制內建物件型別的深拷貝範例:


function deepCopy(obj) {
    let copiedObjects = new WeakMap();
    if (typeof obj !== 'object' || obj === null) {
        return obj;
    }
    if (copiedObjects.has(obj)) {
        return copiedObjects.get(obj);
    }
    let copy;
    if (obj instanceof Date) {
        copy = new Date(obj.getTime());
    } else if (obj instanceof RegExp) {
        copy = new RegExp(obj);
    } else if (Array.isArray(obj)) {
        copy = [];
    } else {
        copy = {};
    }
    copiedObjects.set(obj, copy);
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            if (typeof obj[key] === 'object' && obj[key] !== null) {
                copy[key] = deepCopy(obj[key], copiedObjects);
            } else {
                copy[key] = obj[key];
            }
        }
    }
    return copy;
}

這個範例的深拷貝函數首先檢查當前物件是否為內建物件型別,如果是,則使用相應的建構函式重新建立該物件,否則建立一個普通物件或陣列。然後進行遞迴複製每一個屬性。

需要注意的是,使用建構函式複製內建物件型別只適用於部分內建物件型別,對於其他的內建物件型別,可能需要使用其他的方法來進行復制,或者使用第三方庫來進行復制。

總之,深拷貝複製內建物件型別需要考慮使用建構函式來重新建立物件,如果需要對這些物件進行深拷貝操作,可以使用上述方法或其他庫來實現。

3、特定場景二:自定義物件型別的深拷貝

JavaScript 中自定義物件的深拷貝可以使用同樣的遞迴方式實現,可以使用 WeakMap 方法。

function deepCopy(obj) {
    let copiedObjects = new WeakMap();
    if (typeof obj !== 'object' || obj === null) {
        return obj;
    }
    if (copiedObjects.has(obj)) {
        return copiedObjects.get(obj);
    }
    let copy;
    if (obj instanceof MyCustomObject) {
        copy = new MyCustomObject();
    } else if (Array.isArray(obj)) {
        copy = [];
    } else {
        copy = {};
    }
    copiedObjects.set(obj, copy);
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            if (typeof obj[key] === 'object' && obj[key] !== null) {
                copy[key] = deepCopy(obj[key], copiedObjects);
            } else {
                copy[key] = obj[key];
            }
        }
    }
    return copy;
}

這個函數首先檢查當前物件是否為自定義物件,如果是,則建立一個新的自定義物件,否則建立一個普通物件或陣列。然後進行遞迴複製每一個屬性。

注意,如果自定義物件中包含迴圈參照,需要使用 WeakMap 來避免出現死迴圈。

4、特定場景三:物件中存在函數或迴圈參照

對於函數,通常會忽略它們,因為函數不能被複制,而是需要重新定義。

對於迴圈參照,可以使用 WeakMap 來儲存已經複製過的物件。每次遇到迴圈參照時,可以檢查 WeakMap 中是否已經有該物件的副本,如果有,則直接使用副本,而不是重新建立。

function deepCopy(obj) {
    let copiedObjects = new WeakMap();
    if (typeof obj !== 'object' || obj === null) {
        return obj;
    }
    if (copiedObjects.has(obj)) {
        return copiedObjects.get(obj);
    }
    let copy;
    if (Array.isArray(obj)) {
        copy = [];
    } else {
        copy = {};
    }
    copiedObjects.set(obj, copy);
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            if (typeof obj[key] === 'object' && obj[key] !== null) {
                copy[key] = deepCopy(obj[key], copiedObjects);
            } else {
                copy[key] = obj[key];
            }
        }
    }
    return copy;
}

這是使用 WeakMap 的一種範例,這個範例的深拷貝函數遞迴地複製物件,但檢查 WeakMap 中是否已經存在該物件的副本,如果存在則直接使用副本,而不是重新建立。

此外,使用 JSON.parse(JSON.stringify(obj)) 方法會自動忽略函數和迴圈參照,但是會忽略 undefined 以及正規表示式型別的屬性。

還有,還可以使用第三方庫,如 lodash 中的 _.cloneDeep() 函數、 underscore 中的 _.clone() 函數等來實現物件中存在函數或迴圈參照的深拷貝。

5、特定場景四:物件中有對其他物件的參照或者包含 Symbol 屬性的物件

對於物件中有對其他物件的參照,可以使用 WeakMap 來儲存已經複製過的物件。每次遇到對其他物件的參照時,可以檢查 WeakMap 中是否已經有該物件的副本,如果有,則直接使用副本,而不是重新建立。

對於物件中包含 Symbol 屬性的物件,可以使用 Object.getOwnPropertySymbols() 方法來獲取該物件所有的 Symbol 屬性,然後使用 Object.getOwnPropertyDescriptor() 方法來獲取這些 Symbol 屬性的值,最後將這些值賦給新物件。


function deepCopy(obj) {
    let copiedObjects = new WeakMap();
    if (typeof obj !== 'object' || obj === null) {
        return obj;
    }
    if (copiedObjects.has(obj)) {
        return copiedObjects.get(obj);
    }
    let copy;
    if (Array.isArray(obj)) {
        copy = [];
    } else {
        copy = {};
    }
    copiedObjects.set(obj, copy);
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            if (typeof obj[key] === 'object' && obj[key] !== null) {
                copy[key] = deepCopy(obj[key], copiedObjects);
            } else {
                copy[key] = obj[key];
            }
        }
    }
    let symbols = Object.getOwnPropertySymbols(obj);
    symbols.forEach(symbol => {
        let descriptor = Object.getOwnPropertyDescriptor(obj, symbol);
        Object.defineProperty(copy, symbol, descriptor);
    });
    return copy;
}

這是使用 WeakMap 和 Symbol 屬性的一種範例,這個範例的深拷貝函數首先檢查 WeakMap 中是否已經存在該物件的副本,如果存在則直接使用副本,而不是重新建立。然後使用 Object.getOwnPropertySymbols() 方法獲取該物件所有的 Symbol 屬性,最後使用 Object.getOwnPropertyDescriptor() 方法獲取這些 Symbol 屬性的值,並將這些值賦給新物件。

這種方法可以保證深拷貝物件中包含的所有屬性,包括對其他物件的參照和 Symbol 屬性,但還是不能複製內建物件型別,這些物件型別是不可列舉的。

三、迴圈參照

JavaScript 中的迴圈參照指的是兩個或多個物件之間相互參照的情況。這種情況通常發生在將一個物件賦給另一個物件的屬性時,同時還將另一個物件賦給第一個物件的屬性。

以下是一個範例:

let obj1 = {};
let obj2 = {};

obj1.prop = obj2;
obj2.prop = obj1;

這樣就會產生一個迴圈參照,因為 obj1 和 obj2 相互參照。

迴圈參照可能導致 JavaScript 引擎無法正確處理記憶體,並導致記憶體漏失。因此,在編寫 JavaScript 程式碼時需要特別注意避免迴圈參照。如果您需要在兩個物件之間建立關係,可以使用弱參照來避免迴圈參照。

在深拷貝中遇到迴圈參照就會導致死迴圈,因此需要使用特殊的演演算法來解決這個問題。可以使用遞迴演演算法和深度優先遍歷來實現深拷貝,在遍歷過程中跟蹤已經遍歷過的物件,如果遇到迴圈參照就直接返回已經遍歷過的物件的參照。

 

總的來說,在使用淺拷貝和深拷貝時,需要根據需求和物件的結構來進行選擇。通常來說,如果需要對物件進行修改並且不希望對原物件造成影響,那麼應該使用深拷貝。如果只是需要讀取物件中的資料而不需要修改,那麼可以使用淺拷貝。在實現深拷貝時,需要特別注意迴圈參照和特殊屬性問題。