)
文档/教程前端【免费下载链接】en.javascript.infoModern JavaScript Tutorial项目地址https://gitcode.com/gh_mirrors/en/en.javascript.info点击查看免费下载本文围绕 Modern JavaScript Tutorialen.javascript.info仓库中「Turn the object into JSON and back」练习及其标准解法JSON.parse(JSON.stringify(obj))系统讲解 JSON 序列化与反序列化的完整机制JSON.stringify支持的数据类型、replacer过滤、space格式化、自定义toJSON、JSON.parse的reviver复活函数以及该「深度克隆」惯用法在函数、Symbol、Date 与循环引用面前的安全边界。读完本文你将能独立写出可复用的序列化代码并正确规避 JSON 与 JS 对象互转过程中的所有典型陷阱。一、关联练习与标准解法把对象转成 JSON 再读回来任务原文练习位于仓库 1-js/05-data-types/12-json/1-serialize-object/task.md要求把user对象转成 JSON再读回到另一个变量中let user { name: John Smith, age: 35 };这个任务虽然代码量极小却是理解 JS 中「对象序列化 → 传输/存储 → 反序列化」闭环的起点也是后续 JSON 进阶知识replacer、reviver、toJSON的引子。标准解法JSON.stringify 与 JSON.parse 成对使用官方解法见 1-js/05-data-types/12-json/1-serialize-object/solution.mdlet user { name: John Smith, age: 35 }; let user2 JSON.parse(JSON.stringify(user));执行过程分两步JSON.stringify(user)把对象编码为 JSON 字符串{name:John Smith,age:35}JSON.parse(...)把该字符串解码回一个全新的 JavaScript 普通对象赋值给user2。由此得到的user2与原user结构相同、值相等但是两个独立的对象——修改user2不会影响user。这个「先序列化再反序列化」的模式正是社区中最常见的深度克隆deep clone惯用法对于仅包含纯数据字符串、数字、布尔值、null、数组、普通对象的对象树它能递归地复制出所有嵌套层级一行代码即可完成。⚠️ 需要立刻划清边界这个惯用法只对「JSON 可表示的数据」成立。它既不是万能的深拷贝也不是structuredClone的替代品——函数、Symbol、undefined会被直接丢弃Date会被转成字符串循环引用会直接抛错。这些限制的成因正是下文要展开的 JSON 格式本身。二、JSON.stringify把对象编码为字符串JSONJavaScript Object Notation是一种通用的值/对象表示格式最初为 JavaScript 设计但如今几乎所有语言都提供了 JSON 库因此它天然适合「浏览器端 JavaScript 服务端任意语言Ruby/PHP/Java 等」的数据交换场景。JavaScript 为此提供了两个方法JSON.stringify把对象转换成 JSON 字符串JSON.parse把 JSON 字符串解码回对象。序列化一个学生的完整例子let student { name: John, age: 30, isAdmin: false, courses: [html, css, js], spouse: null }; let json JSON.stringify(student); alert(typeof json); // string alert(json); /* JSON-encoded object: { name: John, age: 30, isAdmin: false, courses: [html, css, js], spouse: null } */得到的json字符串被称为JSON 编码JSON-encoded/ 序列化serialized/ 字符串化stringified/ 封送marshalled的对象可以随时通过网络发送或写入数据存储。JSON 编码与对象字面量的关键差异差异点JS 对象字面量JSON 编码结果字符串引号单引号、双引号、反引号均可只能用双引号如John→John属性名引号可省略必须加双引号如age:30→age:30支持的数据类型JSON.stringify同样适用于原始值JSON 标准只支持以下数据类型对象{ ... }数组[ ... ]原始值字符串、数字、布尔值true/false、null示例alert( JSON.stringify(1) ) // 1 alert( JSON.stringify(test) ) // test字符串仍为字符串但必须是双引号 alert( JSON.stringify(true) ); // true alert( JSON.stringify([1, 2, 3]) ); // [1,2,3]被跳过的 JavaScript 专有属性JSON 是纯数据、语言无关的规范因此JSON.stringify会跳过三类 JavaScript 特有的属性函数属性方法Symbol 键与 Symbol 值值为undefined的属性。let user { sayHi() { // 被忽略 alert(Hello); }, [Symbol(id)]: 123, // 被忽略 something: undefined // 被忽略 }; alert( JSON.stringify(user) ); // {}空对象这解释了「深度克隆」惯用法的第一个边界带方法、Symbol 键或undefined值的对象克隆后会丢失这些成员。从该机制可以推断JSON.parse(JSON.stringify(obj))产出的必然是纯数据普通对象原型链上的方法与继承关系也无法保留——克隆结果只适合纯数据对象。嵌套对象自动递归嵌套对象会被自动递归处理无需手写递归逻辑let meetup { title: Conference, room: { number: 23, participants: [john, ann] } }; alert( JSON.stringify(meetup) ); /* 整个结构都被字符串化 { title:Conference, room:{number:23,participants:[john,ann]}, } */三、硬性限制循环引用必然报错JSON 是树形结构无法表达循环引用。当对象存在环形引用时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这里转换失败的原因正是环形引用room.occupiedBy指向meetup而meetup.place又指向room形成无限递归。仓库 json-meetup.svg 直观展示了这一环状结构这是「深度克隆」惯用法的第二个边界只要对象树中存在环克隆就会直接失败。解决思路不是硬编码删字段而是用下文介绍的replacer参数在序列化过程中把回环引用剔除。四、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不在白名单里。把除引发循环的room.occupiedBy之外的所有属性都列进去alert( JSON.stringify(meetup, [title, participants, place, name, number]) ); /* { title:Conference, participants:[{name:John},{name:Alice}], place:{number:23} } */除了occupiedBy之外都被正确序列化了但属性列表相当冗长维护成本高。函数形式的 replacer按 key 过滤更优雅的做法是传入函数。该函数会对每个(key, value)对调用返回「替换后的值」返回undefined则跳过该属性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, function replacer(key, value) { alert(${key}: ${value}); return (key occupiedBy) ? undefined : value; })); /* 进入 replacer 的 key:value 对 : [object Object] title: Conference participants: [object Object],[object Object] 0: [object Object] name: John 1: [object Object] name: Alice place: [object Object] number: 23 occupiedBy: [object Object] */两个重要细节replacer会拿到包括嵌套对象和数组元素在内的每一对 key/value即递归遍历整棵树函数内部的this指向包含当前属性的那个对象第一次调用是特殊的它使用包装对象{: meetup}发起即第一个(key, value)对是空 key 整个目标对象。这正是上面输出第一行是:[object Object]的原因——这样设计是为了给replacer最大的能力必要时它甚至能分析并替换/跳过整个对象。五、进阶练习按值过滤循环引用而非按属性名原文档紧随其后安排了一道进阶练习 2-serialize-event-circular/task.md它点出了按 key 过滤的不足有些属性名既出现在循环引用中也用于正常属性不能直接按名字排除只能按值判断。题目给出带两处循环引用的对象room.occupiedBy指回meetupmeetup.self也指回meetuplet room { number: 23 }; let meetup { title: Conference, occupiedBy: [{name: John}, {name: Alice}], place: room }; // circular references room.occupiedBy meetup; meetup.self meetup; alert( JSON.stringify(meetup, function replacer(key, value) { /* your code */ }));期望输出中不能包含occupiedBy与self这两个指回meetup的属性但occupiedBy这个属性名同时还是数组[{name:John},...]的名字因此按名排除会误伤数据。官方解法 2-serialize-event-circular/solution.md 改为按值判断alert( JSON.stringify(meetup, function replacer(key, value) { return (key ! value meetup) ? undefined : value; })); /* { title:Conference, occupiedBy:[{name:John},{name:Alice}], place:{number:23} } */解法注释里特别强调必须额外判断key 因为第一次调用时value本身就是meetup那是正常情况不能把它过滤掉。这一行代码可以说是 replacer 使用的点睛之笔——它同时利用了「首调用是包装对象」与「按值比较」两个知识点完整覆盖了原文档讲解的所有 replacer 细节。六、space让输出可读的格式化参数JSON.stringify(value, replacer, space)的第三个参数space指定美化输出pretty printing的空格数。此前所有序列化结果都没有缩进和多余空格——这对网络传输是好事space纯粹用于日志和美观输出。space 2表示嵌套对象换行显示、对象内缩进 2 个空格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 4 时缩进更多 { name: John, age: 25, roles: { isAdmin: false, isEditor: true } } */第三个参数也可以是字符串此时该字符串作为缩进符而不再按空格数量计算。再次强调space只服务于日志与友好输出不影响序列化的数据内容。七、自定义 toJSON控制对象的 JSON 形态就像toString控制字符串转换一样对象可以定义toJSON方法来自定义「转成 JSON 时的形态」JSON.stringify会自动调用它。内置对象中Date自带toJSON因此日期会被序列化为 ISO 字符串let room { number: 23 }; let meetup { title: Conference, date: new Date(Date.UTC(2017, 0, 1)), room }; alert( JSON.stringify(meetup) ); /* { title:Conference, date:2017-01-01T00:00:00.000Z, // (1) Date 内置 toJSON 的产物 room: {number:23} // (2) } */为room添加自定义toJSON后序列化结果随之改变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.stringify(room)时以及room作为嵌套属性出现在其他被编码对象中时。这也是「深度克隆」惯用法的第三个边界自定义了toJSON的对象克隆回来时得到的将是toJSON的返回值形态而非原始结构典型如Date→ 字符串克隆后date不再是Date实例。八、JSON.parse把字符串解码回对象解码 JSON 字符串需要另一个方法JSON.parse(str[, reviver])str要解析的 JSON 字符串reviver可选函数function(key, value)对每个(key, value)对调用可变换解析出的值。解析字符串化数组let numbers [0, 1, 2, 3]; numbers JSON.parse(numbers); alert( numbers[1] ); // 1解析嵌套对象let userData { name: John, age: 35, isAdmin: false, friends: [0,1,2,3] }; let user JSON.parse(userData); alert( user.friends[1] ); // 1手写 JSON 的典型错误JSON 可以任意复杂对象与数组互相嵌套但必须严格遵守 JSON 格式。调试时偶尔需要手写 JSON以下是最常见的错误let json { name: John, // 错误属性名没加引号 surname: Smith, // 错误值用了单引号必须双引号 isAdmin: false // 错误键用了单引号必须双引号 birthday: new Date(2000, 2, 3), // 错误不允许 new只能是裸值 friends: [0,1,2,3] // 这里没问题 };此外JSON 不支持注释加了注释的 JSON 即为非法。虽然存在允许无引号键、注释等扩展的 JSON5 这类独立库但它不在语言规范之内。JSON 之所以如此严格并非规范作者偷懒而是为了换取简单、可靠、极快的解析算法实现。九、reviver反序列化时的「数据复活」设想从服务器拿到一个序列化后的meetup字符串// title: (meetup title), date: (meetup date) let str {title:Conference,date:2017-11-30T12:00:00.000Z};直接JSON.parse后访问日期方法会报错——因为meetup.date只是字符串JSON.parse无从得知该把哪个字符串还原成Datelet meetup JSON.parse(str); alert( meetup.date.getDate() ); // Error!解决方案是传入第二个参数reviver让除date外的所有值原样返回date则转成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() ); // 现在正常了reviver 同样作用于嵌套对象与数组——示例中两个meetups项里的date都被正确还原let schedule { meetups: [ {title:Conference,date:2017-11-30T12:00:00.000Z}, {title:Birthday,date:2017-04-18T12:00:00.000Z} ] }; schedule JSON.parse(schedule, function(key, value) { if (key date) return new Date(value); return value; }); alert( schedule.meetups[1].date.getDate() ); // 正常这一节与第五节互为镜像replacer负责在序列化时按规则改写/剔除字段reviver负责在反序列化时按规则还原/变换字段。若想用JSON.parse(JSON.stringify(obj))克隆带日期的对象务必配合 reviver 把date类字段重新new Date(...)回来否则克隆结果中的日期将退化为字符串。十、总结与实战要点本主题对应的完整原文档为 1-js/05-data-types/12-json/article.md配套练习为 1-serialize-object 与 2-serialize-event-circular。核心结论如下主题要点JSON 是什么语言无关的数据格式有独立标准主流语言均有实现支持的数据普通对象、数组、字符串、数字、布尔值、null两个核心方法JSON.stringify序列化、JSON.parse反序列化二者都支持转换函数自动跳过函数方法、Symbol 键/值、值为undefined的属性必须双引号JSON 中字符串与属性名只能是双引号单引号/反引号非法循环引用直接JSON.stringify会抛错须用 replacer 过滤回环自定义toJSON对象存在toJSON时JSON.stringify自动调用它如Date格式化space参数数字或字符串仅用于日志与美观输出手写 JSON禁止无引号键、单引号、new表达式与注释「JSON 深度克隆」安全使用清单克隆目标须为纯数据对象树含方法、Symbol、undefined、Date的自定义对象会被改写或丢失克隆后通常退化为普通对象对象树中不得存在循环引用否则直接抛错——可通过 replacer 按值剔除回环注意保留首调用需要保留日期语义时用JSON.parse(str, reviver)配合 reviver 还原Date网络传输与日志输出场景下序列化结果紧凑无缩进是默认行为space只在需要人读时使用。掌握了这套「编码—传输—解码」闭环以及replacer/reviver两个转换钩子你就能在任何需要对象深拷贝、数据持久化或前后端交换的场景中写出正确、可控的 JSON 代码。赞分享文档/教程前端【免费下载链接】en.javascript.infoModern JavaScript Tutorial项目地址https://gitcode.com/gh_mirrors/en/en.javascript.info点击查看免费下载相关推荐Modern JavaScript Tutorial 系列深入掌握 JSON 序列化与反序列化JSON.stringify / JSON.parse / toJSON / reviverModern JavaScript Tutorial 系列深入掌握 JSON 序列化与反序列化JSON.stringify / JSON.parse / t文档/教程前端JavaScript 对象方法与 this 关键字详解Modern JavaScript Tutorial 实战篇JavaScript 对象方法与 this 关键字详解Modern JavaScript Tutorial 实战篇 导读 在 JavaScript 中对象文档/教程前端JavaScript 计算器对象实战用 Object Methods 与 this 实现可交互的 calculatorModern JavaScript TutorialJavaScript 计算器对象实战用 Object Methods 与 this 实现可交互的 calculatorModern JavaScript T文档/教程前端上一篇Apache Pulsar 主题压缩Topic Compaction完整实战指南从自动/手动触发到消费者接入下一篇终极Stride引擎地形系统教程从零开始创建逼真开放世界环境创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考