文章末尾配有完整實現程式碼
深複製
什麼是深複製
JavaScript 中的深複製
是建立一個物件或陣列的完整副本,使得原始物件和新物件在記憶體中完全獨立。深複製不僅僅複製物件的第一層屬性,還複製所有巢狀的子物件和子陣列。
這意味著如果你修改了深複製後的物件,原始物件不會受到影響。
為什麼需要深複製
我們在瞭解了什麼是深複製之後,我不禁要問
:“為什麼我們需要深複製呢?他到底哪裏好了?”
深複製的好處
防止意外修改原始資料:在處理複雜的數據結構時,避免意外修改原始物件導致資料錯誤。
資料隔離:在開發過程中,常常需要在多個地方使用相同的數據結構,但希望它們之間完全獨立。
哪些場景需要用到深複製
處理複雜巢狀物件和陣列:當物件包含多個層級的巢狀時,深複製可以確保所有層級的資料都被正確複製。
狀態管理:在前端框架如React中,深複製常用於避免直接修改狀態物件,從而保持UI的穩定性和可預測性。
資料備份和恢復:在某些應用場景下,需要建立物件的副本以備將來恢復使用。
WeakMap
什麼是 WeakMap
WeakMap 是 JavaScript 中和 Map 類似的一種集合型別,一般我們可以在以下場景來使用WeakMap:
快取:使用 WeakMap 可以建立物件快取,而不會阻止被快取物件的垃圾回收。這對於最佳化效能和記憶體管理非常有用。
私有資料儲存:在實現類的私有屬性時,可以使用 WeakMap 儲存與例項相關的私有資料,從而避免直接在物件上新增屬性。
WeakMap 與 Map的區別
鍵型別:Map 的鍵可以是任何型別,而 WeakMap 的鍵必須是物件或非全域性註冊的符號。
垃圾回收:Map 的鍵不會被垃圾回收,而 WeakMap 的鍵是弱引用,可以被垃圾回收。
迭代:Map 支援迭代,而 WeakMap 不支援迭代。
深複製的實現思路
基礎型別
首先,我們需要檢查傳入的物件 obj
是否為 null
或非物件型別(比如基本資料型別:字串、數字、布林值等)。如果是這些情況,我們直接返回這個值,因為基本資料型別沒有巢狀結構,也就不需要深複製。程式碼如下:
// 判斷是否為基礎型別 const isPrimitive = (value) => { return /Number|Boolean|String|Null|Undefined|Symbol|Function/.test( Object.prototype.toString.call(value), ); }; // 原始資料型別及函式 if (isPrimitive(source)) { result = source; }
JS 內建物件
當我們要複製的物件為一些 JS 內建物件
,如 Array、Set、Date、Reg 等的時候,我們可能需要藉助他們的建構函式來重新構造這一屬性或者將其中的屬性進行逐一複製。例如:
// 陣列 if (Array.isArray(source)) { result = source.map((value) => deepClone(value, memory)); // 內建物件Date、Regex } else if (Object.prototype.toString.call(source) === "[object Date]") { result = new Date(source); } else if (Object.prototype.toString.call(source) === "[object Regex]") { result = new RegExp(source); // 內建物件Set、Map } else if (Object.prototype.toString.call(source) === "[object Set]") { result = new Set(); for (const value of source) { result.add(deepClone(value, memory)); } } else if (Object.prototype.toString.call(source) === "[object Map]") { result = new Map(); for (const [key, value] of source.entries()) { result.set(key, deepClone(value, memory)); } }
如何處理迴圈引用
對於物件中的迴圈引用,我們可以使用WeakMap
來記錄已經複製過的物件。在複製過程中,每當遇到一個物件時,先檢查這個物件是否已經存在於WeakMap
中。如果存在,說明這個物件之前已經被複製過,直接返回它的引用即可,避免無限遞迴。
Q: 為什麼 WeakMap
可以用來處理迴圈引用呢?
A: WeakMap 是透過使用“弱引用”來處理迴圈引用的。弱引用意味著如果沒有其他強引用指向同一個物件,這個物件可以被垃圾回收。這對避免記憶體洩漏尤其有用。
在深複製過程中,如果使用普通物件來記錄已複製的物件,當存在迴圈引用時,這些物件的引用會一直存在,無法被垃圾回收。WeakMap 則不同,它不會阻止這些物件被回收。這樣,我們可以在深複製過程中記錄已處理的物件,檢測到迴圈引用時直接返回已有副本,而不會造成記憶體洩漏或無限遞迴。
關鍵在於 WeakMap 提供了一個機制,可以安全地引用物件,同時允許垃圾回收器回收那些不再需要的物件。這在處理複雜數據結構、深複製和迴圈引用時尤為重要。換句話說,WeakMap 幫我們更聰明地管理記憶體。
程式碼實現:
// 簡易示例 if (memory.has(source)) { result = memory.get(source); } else { result = Object.create(null); memory.set(source, result); Object.keys(source).forEach((key) => { const value = source[key]; result[key] = deepClone(value, memory); }); }
完整實現程式碼
function deepClone(source, memory = new WeakMap()) { // 判斷是否為基礎型別 const isPrimitive = (value) => { return /Number|Boolean|String|Null|Undefined|Symbol|Function/.test( Object.prototype.toString.call(value), ); }; let result = null; // 基礎型別及函式 if (isPrimitive(source)) { result = source; // 陣列 } else if (Array.isArray(source)) { result = source.map((value) => deepClone(value, memory)); // 內建物件Date、Regex } else if (Object.prototype.toString.call(source) === "[object Date]") { result = new Date(source); } else if (Object.prototype.toString.call(source) === "[object Regex]") { result = new RegExp(source); // 內建物件Set、Map } else if (Object.prototype.toString.call(source) === "[object Set]") { result = new Set(); for (const value of source) { result.add(deepClone(value, memory)); } } else if (Object.prototype.toString.call(source) === "[object Map]") { result = new Map(); for (const [key, value] of source.entries()) { result.set(key, deepClone(value, memory)); } } else { // 引用型別 if (memory.has(source)) { result = memory.get(source); } else { result = Object.create(null); memory.set(source, result); Object.keys(source).forEach((key) => { const value = source[key]; result[key] = deepClone(value, memory); }); } } return result; }