news 2026/10/7 16:20:06

JSON.stringify 循环引用实战:用 replacer 按值排除反向引用(zh.javascript.info 现代 JavaScript 教程深度解析)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JSON.stringify 循环引用实战:用 replacer 按值排除反向引用(zh.javascript.info 现代 JavaScript 教程深度解析)
  • 文档
  • 教程
  • 前端

【免费下载链接】zh.javascript.info

现代 JavaScript 教程(The Modern JavaScript Tutorial),以最新的 ECMAScript 规范为基准,通过简单但足够详细的内容,为你讲解从基础到高阶的 JavaScript 相关知识。

项目地址:https://gitcode.com/gh_mirrors/zh/zh.javascript.info
点击查看免费下载

本文围绕 zh.javascript.info(现代 JavaScript 教程)中「排除反向引用」这一经典习题展开,完整讲解JSON.stringify在面对循环引用时的报错机理、replacer回调函数的调用契约,以及如何通过按值判断而非按名称判断来优雅剔除循环属性,让原本会抛Converting circular structure to JSON的对象结构顺利序列化。读完本文,你将掌握JSON.stringify(value, replacer, space)三参数的全部用法、replacer函数递归调用与首个包装调用{"": root}的细节,并能在实际项目中正确处理任何自引用、双向引用乃至更深层的环状数据结构。

背景:循环引用是 JSON 序列化的硬伤

JSON 方法,toJSON 一文明确指出JSON.stringify支持嵌套对象的自动转换,但有一条重要限制:不得有循环引用。看这个最典型的双向引用例子:

let room = { number: 23 }; let meetup = { title: "Conference", participants: ["john", "ann"] }; meetup.place = room; // meetup 引用了 room room.occupiedBy = meetup; // room 引用了 meetup JSON.stringify(meetup); // Error: Converting circular structure to JSON

转换直接失败,因为存在环:meetup.place指向room,而room.occupiedBy又指回meetup。JSON 是语言无关的纯数据格式,它没有引用指针的概念,无法表达“同一个对象被引用两次”这一事实,更不可能表达“对象引用自己”。因此遇到环时,引擎只能抛错终止。

教程用示意图直观展示了这个环(见 json-meetup.svg:meetup.place→room,room.occupiedBy→meetup):

好消息是,JSON.stringify提供了第二参数replacer,允许我们在序列化过程中过滤、替换甚至跳过任何属性——这正是破解循环引用的官方出路。

方案一:用属性数组白名单,粗暴但可行

JSON.stringify的完整语法是:

let json = JSON.stringify(value[, replacer, space])
  • value:要编码的值;
  • replacer:要编码的属性数组,或映射函数function(key, value);
  • space:用于格式化的空格数量。

先看数组白名单方案。把replacer传成属性名数组时,只有这些属性会被编码,而且该列表会作用于整个对象结构(包括嵌套层级):

let room = { number: 23 }; let meetup = { title: "Conference", participants: [{name: "John"}, {name: "Alice"}], place: room // meetup 引用了 room }; room.occupiedBy = meetup; // room 引用了 meetup alert( JSON.stringify(meetup, ['title', 'participants']) ); // {"title":"Conference","participants":[{},{}]}

注意participants里的对象变成了{}——因为name不在白名单中。属性列表被递归地应用到整个结构,这正是它的局限:要么列全所有需要的属性,要么漏掉。把name、number也补上,就能得到正确结果:

alert( JSON.stringify(meetup, ['title', 'participants', 'place', 'name', 'number']) ); /* { "title":"Conference", "participants":[{"name":"John"},{"name":"Alice"}], "place":{"number":23} } */

除了引发环的occupiedBy之外,其余属性全部保留。但两个问题显而易见:属性列表冗长(每新增一个字段都要同步维护),而且它是按名称过滤——一旦某个名字既出现在环上、又是业务上必须保留的常规属性,白名单方案就会误伤。

方案二:replacer 函数,按值判断才是正解

JSON.stringify的第二个参数也可以是函数。该函数会为每个(key, value)对调用,并返回“已替换”的值;返回undefined则跳过该属性。

先看教程中按名称过滤的初版:

alert( JSON.stringify(meetup, function replacer(key, value) { return (key == 'occupiedBy') ? undefined : value; }));

这能解决room.occupiedBy这一个环,但正如任务 task.md 强调的:“有时我们不能只使用名称,因为它既可能在循环引用中也可能在常规属性中使用”。假设某个业务对象恰好有一个名为occupiedBy的常规字段,按名称一刀切就会把有效数据也丢掉。因此任务的进阶要求是——通过属性值来检查属性。

完整解答(来自 solution.md)

let room = { number: 23 }; let meetup = { title: "Conference", occupiedBy: [{name: "John"}, {name: "Alice"}], place: room }; // 循环引用 room.occupiedBy = meetup; meetup.self = meetup; alert( JSON.stringify(meetup, function replacer(key, value) { return (key != "" && value == meetup) ? undefined : value; })); /* { "title":"Conference", "occupiedBy":[{"name":"John"},{"name":"Alice"}], "place":{"number":23} } */

核心逻辑一行:

return (key != "" && value == meetup) ? undefined : value;

逐段拆解:

  • value == meetup:只要当前遍历到的属性值恰好就是根对象meetup本身,就返回undefined跳过。这覆盖了两个环:room.occupiedBy(其值正是meetup)和meetup.self(其值也是meetup)。注意这里用的是宽松相等==,对对象而言它和===效果一致(都退化为引用比较),但写法上需与key != ""并列使用。
  • key != "":这是整个解答最容易被忽略、也最关键的一处。为什么必须排除空键?见下文。

为什么必须判断key == ""

replacer函数会获取每个键/值对,包括嵌套对象和数组项,且递归地应用。教程(article.md)特别说明:

第一个调用很特别。它是使用特殊的“包装对象”制作的:{"": meetup}。换句话说,第一个(key, value)对的键是空的,并且该值是整个目标对象。这个理念是为了给replacer提供尽可能多的功能:如果有必要,它有机会分析并替换/跳过整个对象。

也就是说,序列化开始时引擎会以{"": meetup}的形式调用一次replacer。若不做key != ""判断,这一调用中value == meetup成立,整个根对象都会被undefined跳过,输出将变成空——序列化彻底失败。加上空键判断后,首调用按原值放行,真正的过滤从后续递归遍历才开始。

可以在 replacer 里打印每次调用的键值对,亲眼观察递归顺序:

alert( JSON.stringify(meetup, function replacer(key, value) { alert(`${key}: ${value}`); return (key != "" && value == meetup) ? undefined : value; })); /* 传入 replacer 的 key:value 对: : [object Object] ← 首个包装调用 {"": meetup} title: Conference occupiedBy: [object Object],[object Object] 0: [object Object] name: John 1: [object Object] name: Alice place: [object Object] number: 23 occupiedBy: [object Object] ← 值等于 meetup,被跳过 self: [object Object] ← 值等于 meetup,被跳过 */

可见:replacer的this是包含当前属性的对象;数组元素以索引0、1作为键参与回调;place.number这类嵌套属性也会逐一过一遍。最终occupiedBy和self两个“回指 meetup”的属性被剔除,其余数据原样保留,输出干净且无损。

方案对比:白名单 vs 值判断

维度属性数组白名单replacer 函数(按值判断)
过滤依据属性名属性值(可同时结合键名)
维护成本每增删字段都要同步名单无需维护,自动适配结构变化
误伤风险同名但合法的常规属性会被误删无——仅当值等于目标对象时才剔除
嵌套对象名单递归作用于全结构,易漏字段递归遍历每个 (key, value),精准
适用场景结构固定、字段稀少结构复杂、存在多重环/自引用

纵深扩展:把环“修剪”成合法的树

理解了按值过滤的原理后,可以推广出几类实战变体,它们都基于“replacer 返回值决定取舍”这一契约:

1. 剔除所有重复引用(去重而非仅删环)

JSON.stringify(data, function(key, value) { if (typeof value === "object" && value !== null && seen.has(value)) { return undefined; // 该对象已被序列化过一次,跳过 } if (typeof value === "object" && value !== null) seen.add(value); return value; });

适合“多个属性指向同一共享对象、但你又不想重复展开”的场景(如共享配置、缓存引用)。

2. 用占位值替换环,保留结构信息

JSON.stringify(meetup, function(key, value) { if (value === meetup) return "[Circular]"; return value; });

调试日志场景下,用占位字符串代替丢弃,可以让输出自解释“这里有一个回指根对象的引用”。

3. 只回传合法 JSON 类型,主动过滤函数/Symbol/undefined

JSON.stringify默认会静默跳过函数属性、Symbol 键/值以及值为undefined的属性(教程中sayHi、Symbol("id")、something: undefined均被忽略,最终得到{})。若想显式控制,也可以在 replacer 中统一返回undefined:

JSON.stringify(user, function(key, value) { if (typeof value === "function" || typeof value === "symbol" || value === undefined) return undefined; return value; });

其他参数回顾:space 与 toJSON 的配合

replacer之外的另两个能力在排查循环引用问题时也常被用到:

space(第三个参数)——美化输出,便于肉眼审查环被剔除后的结果:

let user = { name: "John", age: 25, roles: { isAdmin: false, isEditor: true } }; alert(JSON.stringify(user, null, 2)); /* 缩进 2 空格: { "name": "John", "age": 25, "roles": { "isAdmin": false, "isEditor": true } } */

space可以是数字(缩进空格数),也可以是字符串(用该字符串做缩进),仅用于日志与美化输出。

toJSON——对象自定义序列化出口。若对象定义了toJSON()方法,JSON.stringify会自动调用它,返回值作为该对象的序列化结果:

let room = { number: 23, toJSON() { return this.number; } }; let meetup = { title: "Conference", room }; alert( JSON.stringify(room) ); // 23 alert( JSON.stringify(meetup) ); // {"title":"Conference","room":23}

利用toJSON也可以从源头“剪断”环——让对象在序列化时只暴露自己的值类型投影,不暴露引用关系。

反方向同样重要:JSON.parse 与 reviver

序列化问题解决后,别忘了配套的反序列化入口JSON.parse(str, [reviver])。reviver函数同样为每个(key, value)对调用,可对值进行转换。典型场景是把服务器返回的时间字符串还原成Date对象:

let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}'; let meetup = JSON.parse(str, function(key, value) { if (key == 'date') return new Date(value); return value; }); alert( meetup.date.getDate() ); // 30,正常运行

它同样递归作用于嵌套结构,例如对schedule.meetups数组内的每个元素还原date字段。stringify的replacer与parse的reviver构成了 JavaScript 中“智能读写 JSON”的完整闭环。

总结

  • 循环引用会让JSON.stringify抛出Converting circular structure to JSON,因为 JSON 纯数据格式无法表达环。
  • 用属性数组做白名单可以避开环,但按名称过滤会误伤同名的常规属性,且名单随结构膨胀难以维护。
  • 终极解法是按值判断:replacer函数中对value等于根对象的属性返回undefined,即可精准剔除所有回指根对象的反向引用(双向引用、自引用一并覆盖)。
  • 必须同时判断key != "":replacer的首个调用以包装对象{"": root}发起,空键调用必须放行,否则整个根对象会被跳过、序列化结果为空。
  • 配合space美化输出、toJSON自定义投影、JSON.parse的reviver还原类型,可构建一整套健壮的 JSON 序列化/反序列化方案。

上述全部代码均可直接复制运行;更多完整示例见 article.md、本题任务描述 与 官方解答,配套的入门练习 将对象转换为 JSON,然后再转换回来 及 解答 也一并收录于 JSON 章节 供对照练习。

  • 文档
  • 教程
  • 前端

【免费下载链接】zh.javascript.info

现代 JavaScript 教程(The Modern JavaScript Tutorial),以最新的 ECMAScript 规范为基准,通过简单但足够详细的内容,为你讲解从基础到高阶的 JavaScript 相关知识。

项目地址:https://gitcode.com/gh_mirrors/zh/zh.javascript.info
点击查看免费下载

相关推荐

上一篇:reth 仓库布局全解:Rust 实现的以太坊节点 Crate 架构导读
下一篇:动物森友会存档编辑器NHSE:5分钟快速入门完整指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 16:18:28

宇树GO2机器狗SLAM建图实战:点云格式解析与转换指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/7 16:17:38

Meson Java 模块实战指南:用 `native_headers()` 自动生成 JNI 头文件

构建工具 【免费下载链接】meson The Meson Build System 项目地址: https://gitcode.com/gh_mirrors/me/meson 点击查看 免费下载 本指南围绕 Meson 构建系统内置的 Java 模块(mesonbuild/modules/java.py)展开,聚焦其核心能力—…

作者头像 李华
网站建设 2026/10/7 16:17:16

前后端分离的会话保存

一、背景什么是前后端分离?指的是后端应用程序和前端HTML代码不在同一个服务器程序中。传统不分离的架构:JavaWeb程序用Tomcat部署,后端程序和HTML资源都在Tomcat的webapps目录里。二、前后端分离的会话保存位置1、第一种,保存在后…

作者头像 李华
网站建设 2026/10/7 16:16:17

Meson Build Options 完全指南:从 meson.options 到内置选项的配置体系

构建工具 【免费下载链接】meson The Meson Build System 项目地址: https://gitcode.com/gh_mirrors/me/meson 点击查看 免费下载 导读:本文以 Meson 构建系统的 Build-options.md 为主体,系统讲解项目自定义构建选项(build opt…

作者头像 李华
网站建设 2026/10/7 16:15:02

MAA 基建换班三种模式的决策链路与自定义排班 JSON 字段行为

MAA 基建换班三种模式的决策链路与自定义排班 JSON 字段行为 【免费下载链接】MaaAssistantArknights 《明日方舟》小助手,全日常一键长草!| A one-click tool for the daily tasks of Arknights, supporting all clients. 项目地址: https://gitcode.…

作者头像 李华