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

手写深拷贝时,怎么用 WeakMap 解决循环引用?

用一张 WeakMap 记录「原对象到副本」的对应关系。每拷贝一个对象,先建好空副本存进表里再去复制属性;之后再遇到同一个原对象,直接返回表里的副本,递归就不会绕圈。

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

靠一张表打断循环

  1. 记录新建副本后立刻 cache.set(原对象, 副本)
  2. 命中递归开头先查表,有记录就直接返回副本
  3. WeakMap键只能是对象,不会阻止原对象被垃圾回收

表只在一次拷贝过程中使用、用完就丢的话,换成 Map 结果完全一样。WeakMap 的优势要在表被长期保存时才体现出来。

核心回答

先记住这个答案

循环引用是指对象的某个属性绕一圈又指回了自己,比如树节点的 parent 指向父节点,父节点的 children 里又有它。基础版深拷贝遇到这种数据会一直递归,最后抛 RangeError。解决办法是给递归加一张表,用 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 copy = Array.isArray(obj) ? [] : {}
  cache.set(obj, copy) // 先登记,再递归
  for (const key of Object.keys(obj)) {
    copy[key] = deepClone(obj[key], cache)
  }
  return copy
}

const root = { name: 'root', children: [] }
const child = { name: 'child', parent: root }
root.children.push(child)
root.self = root

const copy = deepClone(root)
console.log(copy.self === copy) // 自引用指向副本自己
console.log(copy.children[0].parent === copy) // 子节点的 parent 指向新的根
console.log(copy !== root && copy.children[0] !== child)
查看输出与解释
true
true
true

副本不但没有无限递归,还保留了原来的引用关系:原来 root.self 指向 root,副本里 copy.self 指向 copy;原来子节点的 parent 是 root,副本里是新的根。这是因为所有指向 root 的地方,拿到的都是表里同一个副本。

为什么必须先存表再递归

把 cache.set 挪到 for 循环后面,看起来只是顺序不同,实际上缓存完全失效了。

顺序反了会发生什么
拷贝 root 时进入循环,复制 children,再复制 child,child.parent 又指回 root。这时 root 的副本还在循环里没建完,表里还没有它,于是又从头拷贝一遍 root,一直重复下去。所以副本必须在复制属性之前就放进表里,哪怕它现在还是空的。后面绕回来的引用拿到的是这个空副本的引用,等外层循环跑完,它自然就被填满了。
同一个对象被引用多次
不只是环,同一个对象被两个属性引用(比如 a.x 和 a.y 都指向同一个配置对象),带缓存的版本拷贝后 copy.x === copy.y 仍然成立。基础版会把它拷成两份独立的对象,引用关系就变了。

WeakMap 和 Map 在这里差在哪

WeakMap 的键只能是对象,而且对键是弱引用:一个对象除了作为 WeakMap 的键之外没有别的地方再用它,垃圾回收时就可以把它连同这一项一起回收。Map 是强引用,只要 Map 还在,键对象就一直活着。

在上面的写法里,缓存是每次调用 deepClone 时新建的,拷贝结束后没有任何地方引用它,整张表会一起被回收,所以换成 Map 结果和内存表现基本一样。WeakMap 真正有优势的场景是缓存被长期保存,比如把它放在模块顶层反复使用,这时用 Map 会让所有拷过的原对象永远释放不掉。用 WeakMap 更多是一种稳妥的习惯,面试时把这一层说清楚比只说「WeakMap 不会内存泄漏」更有说服力。

回答前,多想一步

容易答错的地方

缓存存的是 true,而不是副本
有人用 WeakSet 或者 cache.set(obj, true) 只记录「处理过」。这样能防止无限递归,但遇到环时拿不到副本,只能返回原对象或者 undefined,副本里又出现了指向原数据的引用,深拷贝就不彻底了。表里必须存副本本身。
以为有了 WeakMap 就不会爆栈
WeakMap 只防环,不防深。一条很长但没有环的链表,递归照样会一层层压栈,层数够多仍然抛 RangeError。要彻底解决只能改成用栈的迭代写法。
试着用自己的话回答

面试官还会怎么问?

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

弱引用的意义是跟着对象的生命周期走,对象不再被使用就能回收。字符串、数字这些原始值没有独立的生命周期,谈不上「被回收」,所以不允许作为键。深拷贝里原始值直接返回,本来也不需要进表。

原生有能处理循环引用的深拷贝吗?

有,structuredClone 能正确处理循环引用和重复引用,结果和上面手写版一致。它不支持函数和 DOM 节点,遇到会抛 DataCloneError,类实例会丢掉原型。JSON.parse(JSON.stringify(obj)) 遇到循环引用会直接抛 TypeError。

能不能给原对象打个标记来代替 WeakMap?

能做到,比如给处理过的对象加一个隐藏属性存副本,但会修改原数据:冻结的对象加不上属性会报错,标记还可能被其它代码遍历到,拷贝完还得清理。WeakMap 在外面单独维护对应关系,不碰原对象,这是它更合适的原因。

从一道题,走向一组知识

把知识连起来

实用函数实现

怎么手写一个递归深拷贝,处理好对象和数组?

基础版递归深拷贝加一个缓存参数就是这一版,先把基础版的类型判断弄清楚再看这里。

实用函数实现

怎么用递归手写一个数组扁平化函数?

扁平化遇到数组引用自己同样会无限递归,也可以用一个集合记录正在处理的数组来防住。

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

参考资料

  • WeakMap

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

本题目录
  1. 先记住这个答案
  2. 带缓存的深拷贝怎么写
  3. 为什么必须先存表再递归
  4. WeakMap 和 Map 在这里差在哪
  5. 容易答错的地方
  6. 面试官还会怎么问
  7. 把知识连起来
读懂,再试着讲出来

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

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