文档教程前端【免费下载链接】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.svgmeetup.place→roomroom.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.mdlet 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/undefinedJSON.stringify默认会静默跳过函数属性、Symbol 键/值以及值为undefined的属性教程中sayHi、Symbol(id)、something: undefined均被忽略最终得到{}。若想显式控制也可以在 replacer 中统一返回undefinedJSON.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点击查看免费下载相关推荐现代 JavaScript 教程用 replacer 按值排除回引用优雅解决 JSON.stringify 循环引用序列化现代 JavaScript 教程用 replacer 按值排除回引用优雅解决 JSON.stringify 循环引用序列化 导读 对象图存在循环引用如 A文档/教程前端en.javascript.info 循环引用序列化实战用 JSON.stringify replacer 按值排除 backreferenceen.javascript.info 循环引用序列化实战用 JSON.stringify replacer 按值排除 backreference 本篇文章基于文档/教程前端JavaScript 循环实战使用 for 循环输出 2 到 10 的偶数zh.javascript.info 教程习题详解JavaScript 循环实战使用 for 循环输出 2 到 10 的偶数zh.javascript.info 教程习题详解 for 循环是 JavaSc文档教程前端上一篇reth 仓库布局全解Rust 实现的以太坊节点 Crate 架构导读下一篇动物森友会存档编辑器NHSE5分钟快速入门完整指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考