淺複製與深複製
淺複製
淺複製是指建立一個新物件,其中複製的是原物件的第一層屬性值。對於原物件的基本資料型別,淺複製會直接複製它們的值;而對於引用型別,淺複製只複製引用,即新物件的屬性和原物件的屬性指向同一個記憶體地址。這樣,如果修改引用型別的內部屬性,原物件和複製物件都會受到影響。
(1)Object.assign()
Object.assign()
靜態方法將一個或者多個源物件中所有可列舉的自有屬性複製到目標物件,並返回修改後的目標物件。Object.assign() 實際上對每個源物件執行的是淺複製
const obj1 = { a: 0, b: { c: 0 } }; const obj2 = Object.assign({}, obj1); console.log(obj2); // { a: 0, b: { c: 0 } } obj1.a = 1; console.log(obj1); // { a: 1, b: { c: 0 } } console.log(obj2); // { a: 0, b: { c: 0 } } obj2.a = 2; console.log(obj1); // { a: 1, b: { c: 0 } } console.log(obj2); // { a: 2, b: { c: 0 } } obj2.b.c = 3; console.log(obj1); // { a: 1, b: { c: 3 } }
(2)擴充套件運算子
let obj1 = {a:1,b:{c:1}} let obj2 = {...obj1}; obj1.a = 2; console.log(obj1); //{a:2,b:{c:1}} console.log(obj2); //{a:1,b:{c:1}} obj1.b.c = 2; console.log(obj1); //{a:2,b:{c:2}} console.log(obj2); //{a:1,b:{c:2}}
程式碼解析:
let obj1 = {a:1,b:{c:1}}; let obj2 = {...obj1};
首先,obj1
是一個巢狀物件,包含一個普通屬性 a
和一個巢狀物件 b
。透過擴充套件運算子,我們將 obj1
的第一層屬性淺複製到了 obj2
中。這意味著 obj2
是一個新物件,但它的屬性值與 obj1
相同。
第一次修改:
obj1.a = 2;
此時,我們修改了 obj1.a
的值為 2
。由於 a
是一個原始型別的值(數字),它的複製是獨立的,因此修改 obj1.a
不會影響到 obj2.a
。
console.log(obj1);
輸出{a:2, b:{c:1}}
console.log(obj2);
輸出{a:1, b:{c:1}}
可以看到,obj1.a
的修改並沒有影響 obj2.a
。
第二次修改:
obj1.b.c = 2;
接下來,我們修改了巢狀物件 b
內的屬性 c
。由於擴充套件運算子進行的是淺複製,所以 obj1.b
和 obj2.b
指向的是同一個引用。當我們修改 obj1.b.c
時,obj2.b.c
也會受到影響。
console.log(obj1);
輸出{a:2, b:{c:2}}
console.log(obj2);
輸出{a:1, b:{c:2}}
此時可以看到,儘管 obj2.a
沒有變化,但 obj2.b.c
已經同步變化了,因為它與 obj1.b
指向同一個物件。
(3)陣列方法實現陣列淺複製
1)Array.prototype.slice
let arr = [1, { a: 1 }]; let copyArr = arr.slice(); copyArr[0] = 2; copyArr[1].a = 2; console.log(arr); console.log(copyArr); // [ 1, { a: 2 } ] // [ 2, { a: 2 } ]
在這個例子中,copyArr[0]
是一個原始值,修改不會影響 arr
,但 copyArr[1]
是一個物件的引用,因此修改 copyArr[1].a
也會影響到原陣列 arr
的同一個物件。
2)Array.prototype.concat
let arr = [1, { a: 1 }]; let copyArr1 = arr.concat();
和 slice()
方法一樣,concat()
也會對陣列中的引用型別元素執行淺複製。
(4)手寫實現淺複製
// 可能是陣列的淺複製或物件的淺複製 function copyShallow(obj) { // 首先判斷傳入型別 let newObj = Array.isArray(obj) ? [] : {}; for(let key in obj) { if(obj.hasOwnProporty(key)) { newObj[key] = obj[key]; } } return newObj; }
深複製
深複製則是遞迴地複製物件的所有層級,包括巢狀的引用型別。深複製建立的是一個完全獨立的新物件,原物件與複製物件之間沒有共享的記憶體區域。因此,修改深複製物件中的任何屬性,都不會影響原物件。
先給出複製的原始物件:
const original = { name: "MDN", money: 123n, b: Symbol('b'), c: null, d: undefined, e() { console.log(1); } }; original.itself = original;
(1)JSON.stringify()
利用 JSON.stringify()
將物件轉換為 JSON 字串,再用 JSON.parse() 將 JSON 字串解析為新的物件。
const clone1 = JSON.parse(JSON.stringify(original)); console.log(clone1);
這種方法簡單易行,但有以下幾個限制:
無法複製BigInt(報錯)
無法處理物件的迴圈引用(報錯)
無法複製Symbol、function、undefined
(2)函式庫lodash的_.cloneDeep方法
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script> <script> var objects = [{ 'a': 1 }, { 'b': 2 }]; var deep = _.cloneDeep(objects); console.log(deep[0] === objects[0]); // => false </script>
(3)structuredClone
(官方深複製API)
const original = { name: "MDN" }; original.itself = original; // Clone it const clone = structuredClone(original); console.log(clone !== original);// true console.log(clone.name === "MDN"); // true console.log(clone.itself === clone); // true console.log(clone);
可以處理迴圈引用
不能複製
Symbol
和function
(4)手寫實現深複製
function deepCopy(obj) { const result = Array.isArray(obj) ? [] : {}; for (let key in obj) { if (obj.hasOwnProperty(key)) { if (obj[key] instanceof Object) { result[key] = deepCopy(obj[key]); } else { result[key] = obj[key]; } } } return result; } const clone3 = deepCopy(original); console.log(clone3);
限制
無法處理迴圈引用(棧溢位錯誤);
不能複製函式;
物件中存在迴圈引用,如果我們一味的遞迴其所有層級,會導致棧溢位錯誤。這裏處理迴圈引用,我們需要使用到ES6
新增的弱引用數據結構 WeakMap
。
程式碼實際並不複雜,難點是要考慮清楚如何藉助弱引用的 WeakMap 處理迴圈引用,避免遞迴導致的溢位。現在我們就藉助weapmap實現一個高階的深複製函式。
高階深複製函式
function deepCopy(obj, hash = new WeakMap()) { if (obj === null || typeof obj !== 'object') { return obj; } if (hash.has(obj)) { return hash.get(obj); } const result = Array.isArray(obj) ? [] : {}; hash.set(obj, result); for (let key in obj) { if (obj.hasOwnProperty(key)) { console.log(obj[key] === original); result[key] = deepCopy(obj[key], hash); } } return result; }
核心邏輯:
檢查是否是物件:函式首先判斷是否是物件或
null
,如果不是,直接返回(處理基本型別)。利用
WeakMap
記錄已複製的物件:每當處理一個物件時,先檢查WeakMap
中是否已存在該物件。如果存在,說明遇到了迴圈引用,直接返回之前記錄的複製。遞迴複製物件的屬性:如果沒有迴圈引用,繼續遞迴複製物件的每個屬性,並在
WeakMap
中記錄當前物件的複製,防止後續遞迴中再次處理到它。
值得一提的是,我們的深複製函式中對於函式的複製在覈心邏輯(1)中被處理,直接返回了原函式,也就是說,複製後的函式是原函式的引用,輸出驗證一下:
console.log(original.e === clone3.e);// true
最後
面試遇到深複製的題目,大家可以回答一下官方的API:structuredClone
,面試官可能覺得你瞭解比較全面,不只是老一套的方法;也可以談談實現深複製函式時需要考慮的情況,比如遞迴複製,弱引用等等。