前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
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 热点
旧版
首页程序员面试题库深拷贝实现 循环引用处理
JavaScript对象与原型

深拷贝和浅拷贝有什么区别?手写深拷贝怎么处理循环引用?

浅拷贝只复制第一层属性,嵌套的对象还是原来那一个,改副本会影响原对象。深拷贝会递归复制每一层,副本和原对象完全独立。手写时用一个 WeakMap 记住「已经拷过的对象」,再遇到它就直接返回之前的副本,循环引用就不会无限递归。

前端进阶之旅 · 一题精讲更新于 2026.09.29
JavaScript#对象与原型
先看核心答案读代码示例
理解线索

三种深拷贝方案各丢什么

  1. JSON.parse(JSON.stringify())丢函数和 undefined,Date 变字符串,遇到循环引用直接报错
  2. structuredClone支持循环引用、Date、Map、Set,但遇到函数会抛错,类实例会变成普通对象
  3. 手写递归加 WeakMap想保留什么自己决定,代价是每种特殊类型都要自己处理

在 WeakMap 里登记副本必须放在递归之前。先递归再登记,循环引用的那一层还没登记上,照样会无限递归。

核心回答

先记住这个答案

浅拷贝只复制对象的第一层。如果某个属性的值是对象,复制过去的只是一个引用,副本和原对象指向同一个嵌套对象,改一边另一边也会变。深拷贝会一层层递归下去,把每个嵌套对象都新建一份,副本和原对象彻底分开。手写深拷贝的难点是循环引用,比如 obj.self = obj,直接递归会无限调用下去直到栈溢出。解决办法是用 WeakMap 记录「原对象到副本」的对应关系,递归前先查一下,拷过的就直接返回副本。

  • 浅拷贝只复制第一层,嵌套对象共用
  • 深拷贝递归复制,副本和原对象互不影响
  • 用 WeakMap 记录已拷贝对象,破解循环引用
  • structuredClone 不能克隆函数

手写一个能处理循环引用的深拷贝

先看完整实现,再和浅拷贝的结果做对比。

递归加 WeakMapJavaScript
function deepClone(value, seen = new WeakMap()) {
  if (value === null || typeof value !== 'object') return value // 原始值直接返回
  if (seen.has(value)) return seen.get(value) // 拷过的直接返回副本
  const copy = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value))
  seen.set(value, copy) // 先登记再递归
  for (const key of Reflect.ownKeys(value)) {
    copy[key] = deepClone(value[key], seen)
  }
  return copy
}

const obj = { tags: ['x'], nested: { n: 1 } }
obj.self = obj // 循环引用
const shallow = { ...obj }
const deep = deepClone(obj)
obj.nested.n = 2
console.log(shallow.nested.n, deep.nested.n)
console.log(deep.self === deep, deep.self === obj)
查看输出与解释
2 1
true false

改了原对象的 nested.n 之后,浅拷贝跟着变成了 2,深拷贝还是 1。deep.self 指向的是副本自己,而不是原对象,说明循环结构也被正确复制了。 关键在 seen.set(value, copy) 这一行。拷贝 obj 时先把它登记上,递归到 obj.self 时发现 obj 已经在表里,就直接返回正在构建的 copy,递归到此为止。这张表只在一次拷贝里用,换成 Map 结果也一样。习惯写 WeakMap,是因为它只接受对象当键、不会阻止键被回收,万一有人把这张表缓存起来复用,也不会造成内存泄漏。 Reflect.ownKeys 能拿到 Symbol 键和不可枚举的键,Object.create(Object.getPrototypeOf(value)) 保留了原型,副本仍能找到原型上的普通方法。但这不等于类实例一定能正常工作:方法若读取 # 私有字段,副本没有那个字段,调用时会抛错。这两点让这个版本比「for...in 加 {}」更完整,却仍不是通用克隆器。

不想手写时怎么选

项目里其实很少需要手写。先看数据长什么样,再选方案。

纯数据、来自接口的 JSON,用 JSON.parse(JSON.stringify(data)) 最省事。但它会丢掉值为 undefined 和函数的属性,Date 会变成字符串,NaN 变成 null,遇到循环引用直接抛 TypeError,遇到 BigInt 也会抛错。

包含 Date、Map、Set、循环引用的数据,用 structuredClone(data)。它是浏览器和 Node.js 都内置的深拷贝函数,用的是和 postMessage 相同的结构化克隆算法。它的限制是不能拷贝函数和 DOM 节点,遇到会抛 DataCloneError;类实例拷贝后原型会丢,变成普通对象;Symbol 键的属性也不会被复制。

需要保留原型、要特殊处理某些类型时,才自己写,或者用 lodash 的 cloneDeep。

回答前,多想一步

容易答错的地方

以为 Object.assign 或展开运算符是深拷贝
Object.assign({}, obj) 和 { ...obj } 都是浅拷贝,只复制第一层。数组的 slice()、concat()、Array.from() 也一样。
用 typeof value === 'object' 判断时忘了 null
typeof null 也是 'object',不先排除 null,后面的 Reflect.ownKeys(null) 会直接报错。
以为手写版能处理所有类型
上面的实现遇到 Date、Map、RegExp 会拷出一个原型正确、但内部数据是空的对象,一调用 getTime() 之类的方法就报错。因为 Date 的时间值保存在引擎内部,不是普通属性,遍历键拿不到。要支持这些类型,得单独判断,比如 new Date(value.getTime())。
试着用自己的话回答

面试官还会怎么问?

为什么用 WeakMap 而不是普通对象做缓存?

普通对象的键只能是字符串或 Symbol,对象当键会被转成 '[object Object]',所有对象都撞在一起。Map 可以用对象当键,一次性的拷贝里用它完全没问题,只是它会强引用这些对象。WeakMap 既能用对象当键,又不影响垃圾回收,缓存一旦被长期保留也更安全。

深拷贝会不会栈溢出?

循环引用解决了,但嵌套层级特别深的数据,比如一条几万层的链表,递归还是会爆栈。这种情况要把递归改成用数组模拟栈的循环写法。

函数需要深拷贝吗?

一般不需要,直接复用同一个函数引用即可。函数的闭包环境没法复制,硬要复制只能用 toString 再重新求值,会丢掉闭包,不推荐。

从一道题,走向一组知识

把知识连起来

对象与原型

{ ...obj } 会复制哪些属性?不可枚举的、原型上的会被复制吗?

{ ...obj } 就是最常用的浅拷贝,它复制哪些属性、不复制哪些属性,决定了浅拷贝的边界。

对象与原型

class 的静态方法会被子类继承吗?子类里怎么调用父类的静态方法?

深拷贝时用 Object.getPrototypeOf 保留原型,和子类通过原型链找到父类静态方法是同一套原型机制。

参考资料

  • JavaScript:对象与原型参考资料

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

本题目录
  1. 先记住这个答案
  2. 手写一个能处理循环引用的深拷贝
  3. 不想手写时怎么选
  4. 容易答错的地方
  5. 面试官还会怎么问
  6. 把知识连起来
读懂,再试着讲出来

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

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