文章末尾配有完整实现代码
深拷贝
什么是深拷贝
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; }