JSON.stringify的replacer函数无法自动识别循环引用,必须配合WeakMap等外部状态手动追踪已访问对象,通过检查对象是否重复出现来拦截并替换为占位符,否则仍会抛出TypeError。
replacer 函数怎么识别循环引用它根本识别不了——JSON.stringify 在遇到循环引用时直接抛错 TypeError: Converting circular structure to JSON,replacer 函数压根不会被调用。这是关键前提:你得先拦截循环,不能指望 replacer 自己“发现”并绕过。
replacer 实现循环检测必须配合外部状态你需要一个可追踪已访问对象的容器(比如 WeakMap),在 replacer 每次被调用时检查当前值是否已出现过:
const seen = new WeakMap();function replacer(key, value) { if (typeof value === 'object' && value !== null) { if (seen.has(value)) { return '[Circular]'; } seen.set(value, true); } return value;}JSON.stringify(obj, replacer);
WeakMap 是必须的——用 Map 或数组存引用会阻止 GC,造成内存泄漏object 类型做检查,避免干扰 undefined、function 等本就会被忽略的值'[Circular]' 是常见约定,也可返回 null 或自定义结构(如 { $ref: 'id123' })replacer 的执行顺序容易误判replacer 是深度优先、从内到外执行的:子属性先处理,父对象后处理。这意味着如果某个对象既出现在 A 的子树,又出现在 B 的子树,而 A 先被遍历,B 后遍历,那 B 中对该对象的引用仍会被标记为循环——但实际它可能只是共享引用,不是严格意义上的“循环”。
key 链),而不仅是对象本身replacer 不暴露当前路径,只能靠闭包维护栈(例如用数组 push/pop 当前 key)WeakMap 方案已足够;过度追求路径精确反而让逻辑难以维护replacer 更可靠真正复杂的循环结构(如带元数据、需要反序列化还原、含函数或 Symbol)不该硬塞进 JSON.stringify。更务实的做法:
flatted(支持循环,输出仍是 JSON 字符串)或 cycle(生成带 $ref 的结构)console.dir(obj, { depth: null }),它内部已处理循环硬写 replacer 处理多层嵌套+交叉引用,很快会陷入状态管理混乱——那个 WeakMap 清不清理?不同调用之间要不要复用?这些细节比逻辑本身更容易出错。