手动编写递归深拷贝函数解决循环引用,关键在于提前拦截和缓存复用:用weakmap记录源对象与拷贝对象映射,每次先查缓存,已有则直接返回;精准判断类型,单独处理null,用object.prototype.tostring.call获取精确类型标签,分类型构造新实例并递归填充,遍历仅限自身可枚举属性及symbol属性。

手动编写递归深拷贝函数解决循环引用,关键在于“提前拦截”和“缓存复用”——每次进入拷贝前先查缓存,已处理过的对象直接返回,不重复递归。
用 WeakMap 记录源对象与拷贝对象的映射
WeakMap 是最佳选择:键必须是对象,且不阻止垃圾回收,避免内存泄漏。不能用普通 Object 或 Map(后者会强持有键,导致无法释放)。
- 初始化一个 const seen = new WeakMap(),在函数外层或递归闭包中维护
- 每次调用函数时,先用 seen.get(obj) 查是否已有拷贝结果
- 如果有,直接返回该拷贝;如果没有,先 seen.set(obj, clone) 存入占位对象(如 {} 或 []),再填充属性
精准判断类型,避免误判 null 和类数组
typeof null === 'object' 是历史遗留 bug,必须单独处理;Array.isArray() 比 instanceof 更可靠,能正确区分真实数组和类数组对象。
- 原始类型(string/number/boolean/null/undefined/Symbol/BigInt)直接返回
- null 单独判断:if (obj === null) return null
- 用 Object.prototype.toString.call(obj) 获取精确类型标签,例如 '[object Date]'、'[object RegExp]'
分类型构造新实例并递归填充
不同内置类型不能统一用 {} 或 [] 创建,否则会丢失类型信息和行为(比如 new Date() 和普通对象完全不同)。
- Date → new Date(obj.getTime())
- RegExp → new RegExp(obj.source, obj.flags)
- Map → new Map([...obj].map(([k, v]) => [deepClone(k), deepClone(v)]))
- Set → new Set([...obj].map(item => deepClone(item)))
- TypedArray(如 Uint8Array)→ new obj.constructor(obj) 或 obj.constructor.from(obj)
递归填充对象或数组时注意属性可枚举性
只遍历自身可枚举属性,跳过原型链上的属性,避免污染拷贝结果。
- 用 Object.keys(obj) 或 for...in + obj.hasOwnProperty(key)
- 不要用 for...in 单独遍历,否则会把原型方法也拷过去
- Symbol 属性需额外通过 Object.getOwnPropertySymbols(obj) 获取并处理











