前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
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 热点
旧版
首页程序员面试题库JSON.stringify 循环引用报错 解决方案
JavaScript数据处理

JSON.stringify 遇到循环引用会报什么错?除了手动删属性还有什么办法?

对象的某个属性指回自己或者祖先时,JSON.stringify 会抛 TypeError,提示 Converting circular structure to JSON。先想清楚目的:打日志可以用 replacer 把环替换成 [Circular] 标记;只是想深拷贝,直接用 structuredClone,它能保留循环关系;要通过接口传完整的对象图,就得设计 ID 引用协议。

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

JSON 是树,对象可以是图

  1. JSON只能表示树状结构,文本里没有「引用同一个对象」的写法
  2. 循环引用某条属性路径绕回了自己的祖先,遍历会永远走不完
  3. 共享引用两个属性指向同一个对象,但没有绕回祖先,不算环

共享引用 JSON.stringify 能正常处理,只是解析回来会变成两个独立的对象。很多「去环」写法分不清这两种情况,把共享的那份数据也替换掉了。

核心回答

先记住这个答案

JSON.stringify 递归遍历对象时,如果某个属性又指回了当前路径上的祖先,比如 user.self = user,遍历就会无限进行下去。引擎检测到这种情况会抛 TypeError,Chrome 和 Node 里的提示是 Converting circular structure to JSON。解决办法取决于目的:打日志,用 replacer 把环替换成标记,接受信息有损;深拷贝,用 structuredClone,它原生支持循环引用;通过网络传完整的对象图,就要设计 $id 和 $ref 这样的引用协议,因为 JSON 本身只能表示树。

  • 循环引用让 JSON.stringify 抛 TypeError
  • WeakSet 版 replacer 简单,但会把共享引用误当成环
  • 只是深拷贝就用 structuredClone,它支持循环引用
  • JSON 只能表示树,要传图得自己定引用协议

最常见的写法:WeakSet 记录见过的对象

日志上报、错误监控里经常要把一个复杂对象打出来,里面可能有组件实例、Express 的 req 这类带循环引用的对象,比如 req.res.req 又绕回了自己。先让它不报错再说。

报错和 WeakSet 版 replacerJavaScript
const user = { name: 'Ada' }
user.self = user // 指回自己

try {
  JSON.stringify(user)
} catch (err) {
  console.log(err.name)
}

const seen = new WeakSet()
const json = JSON.stringify(user, (key, value) => {
  if (typeof value === 'object' && value !== null) {
    if (seen.has(value)) return '[Circular]' // 见过的对象不再展开
    seen.add(value)
  }
  return value
})
console.log(json)
查看输出与解释
TypeError
{"name":"Ada","self":"[Circular]"}

replacer 会对每个属性调用一次。第一次遇到一个对象就记下来,第二次再遇到就返回一个字符串标记,不再往里展开,遍历就能结束了。 用 WeakSet 而不是 Set,是因为它只存对象的弱引用,不会阻止对象被垃圾回收,语义上也正好是「见过这个对象」。不过这次 stringify 结束后 seen 就没用了,两者在这里差别不大。 要注意的是,输出里的 [Circular] 只是一个普通字符串,JSON.parse 之后不会重新连回 user。所以这种做法只适合日志这类「能看就行」的场景。

共享引用被误伤了

上面的写法有个问题:它只要见过一个对象,第二次遇到就当成环。但同一个对象被两个属性引用,不一定是环。

用祖先栈只拦真正的环JavaScript
function safeStringify(obj) {
  const ancestors = [] // 当前路径上的祖先
  return JSON.stringify(obj, function (key, value) {
    if (typeof value !== 'object' || value === null) return value
    // this 是当前属性所在的对象,弹出不在这条路径上的祖先
    while (ancestors.length > 0 && ancestors.at(-1) !== this) ancestors.pop()
    if (ancestors.includes(value)) return '[Circular]'
    ancestors.push(value)
    return value
  })
}

const address = { city: '杭州' }
const order = { from: address, to: address } // 共享引用,不是环
order.owner = order // 这才是环
console.log(safeStringify(order))
查看输出与解释
{"from":{"city":"杭州"},"to":{"city":"杭州"},"owner":"[Circular]"}

from 和 to 指向同一个地址对象,但 to 并没有绕回祖先,原生 JSON.stringify 本来就能处理它。WeakSet 版会把 to 替换成 [Circular],地址信息就丢了。 祖先栈的思路是:只记录「当前这条路径」上的对象。replacer 里的 this 指向当前属性所在的对象,所以栈顶不是 this 时,说明已经回到了上层,把多余的弹出去。这样只有真正绕回祖先的引用才会被标记。注意这里必须用普通函数,箭头函数拿不到 this。

按目的选方案

先问清楚为什么要序列化,再选方案。

只是想深拷贝:structuredClone
很多人遇到这个报错,是因为在用 JSON.parse(JSON.stringify(obj)) 做深拷贝。structuredClone(obj) 是浏览器和 Node 17 起内置的深拷贝函数,能正确处理循环引用,拷贝出来的 self 指向拷贝后的新对象。它还能拷贝 Date、Map、Set 这些 JSON 处理不了的类型。函数和 DOM 节点不能拷贝,会抛 DataCloneError。
要通过接口传完整对象图:引用协议
JSON 没有引用语法,要传图结构只能自己编码:第一次出现的对象带一个 $id,之后出现的地方写成 { "$ref": "1" }。接收端先把所有对象建出来,再把 $ref 换成真实引用。这需要前后端共同约定,不能只在前端偷偷做。
回答前,多想一步

容易答错的地方

用 JSON 往返做所有深拷贝
除了循环引用会报错,JSON 往返还会把 Date 变成字符串,把 Map、Set 变成空对象,把 undefined 属性直接删掉。深拷贝优先用 structuredClone,它处理不了的类型再按业务单独处理。
看到对象第二次出现就认定是环
共享引用和循环引用是两回事。WeakSet 版的写法会把共享的那份数据丢掉,只能用于日志。需要尽量保留数据时,要用祖先栈这种只看当前路径的写法。
试着用自己的话回答

面试官还会怎么问?

Vue 或 React 的状态对象打日志为什么经常报这个错?

框架内部的对象本身就有互相指向的属性:Vue 组件实例有 $parent 和 $root,React 的 Fiber 用 return 和 child 连成树,还会通过 __reactFiber$ 开头的属性挂在 DOM 节点上。把 ref、事件对象或者整个组件实例直接 stringify,就会碰到环。打日志时应该只挑需要的字段,而不是把整个对象扔进去。

有没有现成的库?

flatted 可以把带循环的对象编码成能还原的字符串,适合需要往返的场景。只是打日志的话,Node 的 util.inspect() 和浏览器的 console.log 本身就能显示循环引用,会标成 [Circular *1] 这样的记号,不需要先转成 JSON。

JSON.stringify 为什么选择报错,而不是自动跳过环?

规范要求序列化时维护一个当前正在处理的对象栈,遇到栈里已经有的对象就抛 TypeError。选择报错而不是跳过,是因为跳过会悄悄丢数据,调用方根本不知道输出不完整。

从一道题,走向一组知识

把知识连起来

数据处理

JSON.stringify 遇到 NaN 和 Infinity 会怎样?为什么不报错?

循环引用会让 JSON.stringify 报错,NaN 却会悄悄变成 null,一个吵一个静,都是序列化时丢信息的地方。

数据处理

接口返回的日期是字符串,怎么用 JSON.parse 的 reviver 转回 Date?

用 ID 引用协议传对象图时,接收端要在 JSON.parse 之后把引用重新连起来,和 reviver 恢复类型是同一类工作。

参考资料

  • JavaScript:数据处理参考资料

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

本题目录
  1. 先记住这个答案
  2. 最常见的写法:WeakSet 记录见过的对象
  3. 共享引用被误伤了
  4. 按目的选方案
  5. 容易答错的地方
  6. 面试官还会怎么问
  7. 把知识连起来
读懂,再试着讲出来

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

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