前端进阶之旅前端进阶之旅
  • 基础篇HTML/CSS/JS 打底
  • 进阶篇原理与工程化
  • 高频篇面试最常问的那批
  • 精选篇按模块收敛的总结
  • 手写篇常考代码手写实现
  • 面经篇真实面试问题复盘
  • AI 篇NEWAI 时代的前端考点
  • 历年面经NEW按年份追踪真实考点
  • 每日一题每天一道,攒手感
  • 专项自测100 题快速查漏
  • 小程序题库小程序专项刷题
  • 算法题库NEW在线编码即时判题
  • 知识卡片NEW碎片时间过考点
  • 面试题大全常见问题解析
  • AI 答疑NEW随时提问,即时解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • AI 定制路线NEW按你的简历现排
  • AI 知识地图NEW串起全站知识点
  • 原理篇React / Vue 源码拆解
  • HTTP从报文一路讲到 HTTPS
  • 浏览器渲染、事件循环、进程
  • 计算机基础Linux、网络、操作系统
  • 设计模式23 种模式怎么用
  • Node学习指南从环境搭建到服务端
  • NPM工作流script、依赖与发布
  • Docker容器化部署上手
  • Canvas图形与动画实战
  • 前端系统进阶学习大型项目工程化
  • 前端综合文章长期沉淀的实践文
  • 思维导图知识点全景图
  • 学习路线按图索骥不跑偏
  • AI 热点NEWAI 每日动态
  • 公众号动态公众号历史文章
  • 博客动态站长的技术博客
  • 开发者导航常用工具与文档站
  • 基础篇HTML/CSS/JS 打底
  • 进阶篇原理与工程化
  • 高频篇面试最常问的那批
  • 精选篇按模块收敛的总结
  • 手写篇常考代码手写实现
  • 面经篇真实面试问题复盘
  • AI 篇NEWAI 时代的前端考点
  • 历年面经NEW按年份追踪真实考点
  • 每日一题每天一道,攒手感
  • 专项自测100 题快速查漏
  • 小程序题库小程序专项刷题
  • 算法题库NEW在线编码即时判题
  • 知识卡片NEW碎片时间过考点
  • 面试题大全常见问题解析
  • AI 答疑NEW随时提问,即时解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • AI 定制路线NEW按你的简历现排
  • AI 知识地图NEW串起全站知识点
  • 原理篇React / Vue 源码拆解
  • HTTP从报文一路讲到 HTTPS
  • 浏览器渲染、事件循环、进程
  • 计算机基础Linux、网络、操作系统
  • 设计模式23 种模式怎么用
  • Node学习指南从环境搭建到服务端
  • NPM工作流script、依赖与发布
  • Docker容器化部署上手
  • Canvas图形与动画实战
  • 前端系统进阶学习大型项目工程化
  • 前端综合文章长期沉淀的实践文
  • 思维导图知识点全景图
  • 学习路线按图索骥不跑偏
  • AI 热点NEWAI 每日动态
  • 公众号动态公众号历史文章
  • 博客动态站长的技术博客
  • 开发者导航常用工具与文档站
首页程序员面试题库深拷贝 循环引用 WeakMap
前端前端手写题实用函数实现

手写深拷贝时如何用 WeakMap 解决循环引用问题?

在递归深拷贝里维护一张“原对象引用→副本对象”的哈希表,一旦对象之前出现过,就直接返回已存副本,从而切断循环通路;选 WeakMap 是为了让这张表不拖住原对象的回收。

前端进阶之旅 · 一题精讲更新于 2026.09.05
前端手写题#实用函数实现
先看核心答案读代码示例
理解线索

WeakMap 如何承担缓存角色

  1. 缓存时机先 set 再递归,防止后续自身引用
  2. 缓存特点对象键弱引用,不阻止 GC
  3. 递归出口命中已存对象时直接返回副本

只处理对象/数组的复制,不实现 Date、RegExp 等特殊对象分支。

核心回答

先记住这个答案

手写递归深拷贝时,每进入一个对象先创建空副本,并把“源对象→空副本”存入 WeakMap,之后遍历属性。若属性值回到任意已处理的源对象,WeakMap 命中后直接返回该副本,不会再次开辟新路径。选 WeakMap 而不选 Map,是因为对象作为键时可随原对象生命周期被回收,Map 默认强引用会让缓存长期占用内存。

  • 必须先把空副本存入 WeakMap 再递归
  • WeakMap 键用对象引用,不阻止回收
  • 循环引用时直接返回已记录副本

先登记后递归——WeakMap 在闭环中的位置

深拷贝的递归进入对象前,先创建一个空副本并用 WeakMap 记录“原对象→副本”。随后递归复制各属性;一旦子值回到某个已处理对象,WeakMap 命中,函数直接返回旧副本而不再进入。这样自环、互相引用都会合入到同一份副本,不会因再次 create 而触发无限栈扩展。

Map 同样能存出这种对应关系,但它对键是强引用,深拷贝缓存会与源对象存续同样长的时间;若拷贝后原对象不再使用,Map 的键仍使其无法被回收。WeakMap 的键是弱引用,对象失去外部引用后,该缓存项也可被回收,因此适合只存活在拷贝流程中的已处理对象名单。

深拷贝使用 WeakMap 截断环JavaScript
function deepClone(obj, cache = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') return obj;
  if (cache.has(obj)) return cache.get(obj);
  const clone = Array.isArray(obj) ? [] : {};
  cache.set(obj, clone);
  for (const key of Object.keys(obj)) {
    clone[key] = deepClone(obj[key], cache);
  }
  return clone;
}

const solo = { text: 'self' };
solo.self = solo;
const copySolo = deepClone(solo);
console.log('self-cycle resolved:', copySolo.self === copySolo);

const left = { name: 'left' };
const right = { name: 'right' };
left.toRight = right;
right.toLeft = left;
const copyLeft = deepClone(left);
console.log('mutual-cycle resolved:', copyLeft.toRight.toLeft === copyLeft);

const nestedObj = { users: [{ id: 1 }, { id: 2 }] };
const nestedCopy = deepClone(nestedObj);
nestedCopy.users[0].id = 9;
console.log('nested copied:', nestedObj.users[0].id === 1 && nestedCopy.users[0].id === 9);
查看输出与解释
self-cycle resolved: true
mutual-cycle resolved: true
nested copied: true

时间复杂度 O(n),每个对象属性在缓存命中后只会访问一次;空间复杂度 O(n),用于 WeakMap 和递归辅助栈。三个输入分别验证自引用、互相引用和深层嵌套数组的独立性。

工程错误:递归完成才登记副本的现场

假设拷贝一棵按引用连成全环的树:根节点挂左子树,左子树的 parent 指回根。若代码先递归赋值再执行 cache.set,第一次进入左子树时根节点还未存入缓存,根节点又会构造第二份副本,导致递归出不去。最终爆栈或尾部无限增长;解决这类结构化数据需要先 set 后拷贝,副本先落位再填充属性。

真实场景常常是表单数据模型具备 parent、referrer 这类反向指针。WeakMap 的键必须是对象,因此实例、数组都天然满足;数组需要判成 Array.isArray 得到新的数组容器,否则缓存的值类型会错误,处理指针时会看到结构仍是对象,但对循环闭环本身无影响。测试用例分别用自环、互链确认了克隆闭环与原对象不共享。

WeakMap 不解决深度,也不解决特殊类型

本解法最明显的失效条件是用于非普通对象:Date、RegExp、Map、Set 会被当作普通对象,创建空对象后得到没有内置方法的假副本。题目不要求叠加这些类型,若实际使用要在 deepClone 入口先处理特殊构造函数,或者在调用前验收允许复制的载荷中不含有这些实体。

另一个失效边界是超长链而非环:WeakMap 只防无限递归,不防调用栈增长,5000 层以上嵌套依然可能超限。代价小的办法是改用显式栈迭代深拷贝;代价稍大但稳定的是使用 structuredClone,或将数据交给 Worker 做结构化克隆。

回答前,多想一步

容易答错的地方

功能相同所以可随意选 Map
两者都能记映射,但 Map 强引用使源对象不能回收,WeakMap 弱引用源对象在拷贝后被移除时可随缓存一起去掉。只做缓存防环选 WeakMap,需要遍历或 size 统计时才转 Map。
先 set 或后 set 无所谓
顺序会影响能否跳出循环。进入对象时必须先存空副本再处理属性;若先递归,环内第二次遇到相同对象时缓存里还没有记录,会重新创建节点,递归无法结束。
试着用自己的话回答

面试官还会怎么问?

WeakMap 的键为什么不能是字符串或数字?

WeakMap 为弱引用而设计,只有对象或非全局 Symbol 才有可被 GC 跟踪的生命周期。原始值没有独立的生命期,无法由垃圾回收决定清理时机,深拷贝中检测到原始值直接返回即可。

如果不使用 WeakMap,用 Map 会出现什么副作用?

Map 会对 key 形成强引用,让已拷贝对象在缓存存在期间持续占用内存;若源对象不再需要,无法被回收。拷贝完成后若不主动 clear,长时间驻留的模块可能累积不必要的内存。WeakMap 不存在这一问题,但也因此不能枚举或查看 size。

能否通过给对象添加标记属性来替代 WeakMap?

可行但副作用明显:给对象写额外属性会污染源数据,冻结或不可扩展对象会抛错,且可能与原型链逻辑纠缠。WeakMap 从外部维护映射,对用户不可见,代码更干净。

从一道题,走向一组知识

把知识连起来

实用函数实现

手写 JavaScript 深拷贝:如何处理普通对象与数组的递归克隆?

同属「实用函数实现」专题,接着看 JavaScript 深拷贝 手写 在具体场景中的处理方式。

实用函数实现

手写数组扁平化:如何用递归实现任意深度嵌套数组的拍平?

同属「实用函数实现」专题,接着看 数组扁平化 手写 flat 在具体场景中的处理方式。

放回知识体系中复习JavaScript 进阶知识

参考资料

  • WeakMap

示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。

本题目录
  1. 先记住这个答案
  2. 先登记后递归——WeakMap 在闭环中的位置
  3. 工程错误:递归完成才登记副本的现场
  4. WeakMap 不解决深度,也不解决特殊类型
  5. 容易答错的地方
  6. 面试官还会怎么问
  7. 把知识连起来
读懂,再试着讲出来

先看核心答案,再读代码。最后展开追问,检查自己有没有遗漏边界。

试着回答追问
浏览全部面试题理解原理,也关注真实的使用场景。回到顶部 ↑