news 2026/10/4 10:56:56

JavaScript 对象与 JSON 互转实战:JSON.stringify / JSON.parse 深度解析与深度克隆(Modern JavaScript Tutorial 精读)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JavaScript 对象与 JSON 互转实战:JSON.stringify / JSON.parse 深度解析与深度克隆(Modern JavaScript Tutorial 精读)
  • 文档/教程
  • 前端

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

Modern JavaScript Tutorial

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

本文围绕 Modern JavaScript Tutorial(en.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.md:

let user = { name: "John Smith", age: 35 }; let user2 = JSON.parse(JSON.stringify(user));

执行过程分两步:

  1. JSON.stringify(user)把对象编码为 JSON 字符串:{"name":"John Smith","age":35};
  2. JSON.parse(...)把该字符串解码回一个全新的 JavaScript 普通对象,赋值给user2。

由此得到的user2与原user结构相同、值相等,但是两个独立的对象——修改user2不会影响user。这个「先序列化再反序列化」的模式,正是社区中最常见的深度克隆(deep clone)惯用法:对于仅包含纯数据(字符串、数字、布尔值、null、数组、普通对象)的对象树,它能递归地复制出所有嵌套层级,一行代码即可完成。

⚠️ 需要立刻划清边界:这个惯用法只对「JSON 可表示的数据」成立。它既不是万能的深拷贝,也不是structuredClone的替代品——函数、Symbol、undefined会被直接丢弃,Date会被转成字符串,循环引用会直接抛错。这些限制的成因,正是下文要展开的 JSON 格式本身。

二、JSON.stringify:把对象编码为字符串

JSON(JavaScript 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] */

两个重要细节:

  1. replacer会拿到包括嵌套对象和数组元素在内的每一对 key/value,即递归遍历整棵树;
  2. 函数内部的this指向包含当前属性的那个对象;
  3. 第一次调用是特殊的:它使用包装对象{"": meetup}发起,即第一个(key, value)对是空 key + 整个目标对象。这正是上面输出第一行是":[object Object]"的原因——这样设计是为了给replacer最大的能力,必要时它甚至能分析并替换/跳过整个对象。

五、进阶练习:按值过滤循环引用(而非按属性名)

原文档紧随其后安排了一道进阶练习 2-serialize-event-circular/task.md,它点出了按 key 过滤的不足:有些属性名既出现在循环引用中,也用于正常属性,不能直接按名字排除,只能按值判断。

题目给出带两处循环引用的对象(room.occupiedBy指回meetup,meetup.self也指回meetup):

let 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无从得知该把哪个字符串还原成Date:

let 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 深度克隆」安全使用清单:

  1. 克隆目标须为纯数据对象树:含方法、Symbol、undefined、Date的自定义对象会被改写或丢失,克隆后通常退化为普通对象;
  2. 对象树中不得存在循环引用,否则直接抛错——可通过 replacer 按值剔除回环(注意保留首调用);
  3. 需要保留日期语义时,用JSON.parse(str, reviver)配合 reviver 还原Date;
  4. 网络传输与日志输出场景下,序列化结果紧凑无缩进是默认行为,space只在需要人读时使用。

掌握了这套「编码—传输—解码」闭环以及replacer/reviver两个转换钩子,你就能在任何需要对象深拷贝、数据持久化或前后端交换的场景中写出正确、可控的 JSON 代码。

  • 文档/教程
  • 前端

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

Modern JavaScript Tutorial

项目地址:https://gitcode.com/gh_mirrors/en/en.javascript.info
点击查看免费下载
上一篇:Apache Pulsar 主题压缩(Topic Compaction)完整实战指南:从自动/手动触发到消费者接入
下一篇:终极Stride引擎地形系统教程:从零开始创建逼真开放世界环境

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

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

STM32驱动MRAM MR25H40CDF:工业设备掉电保存的高可靠实践

这几年我做的几个工控小项目里,掉电保存这件事一直躲不开。数控机床的刀具补偿参数、伺服驱动器的 PID 整定值、生产线传感器的标定系数,这些数据既要随时改,又不能在掉电时丢。最开始大家图省事直接用 SPI Flash,后来发现某些参数…

作者头像 李华
网站建设 2026/10/4 10:53:57

OpenShell使用指南:让Windows 11恢复经典开始菜单

去年帮公司做了一轮办公电脑的系统迁移,一批机器从 Windows 7 直接跨到 Windows 11。系统装完的第二天,办公室就炸了锅——不是新系统跑不动,而是开始菜单彻底变了样。磁贴布局、右键菜单还要多点一层"显示更多选项",几…

作者头像 李华
网站建设 2026/10/4 10:53:53

Java AIO与MQTT百万级长连接实战:从线程模型到Broker调优全解析

简介:基于 Java 异步 IO(AIO)技术打造的高性能消息队列遥测传输(MQTT)客户端与服务端组件,专为物联网、边缘计算及消息服务器开发者设计,旨在解决海量设备接入与低延迟消息转发的工程难题。项目…

作者头像 李华
网站建设 2026/10/4 10:53:30

Flutter鸿蒙堆叠布局实战:Stack、角标与卡片叠加方案

从Flutter切到鸿蒙应用开发之后,我最大的感受是:UI组件层面的思路完全通用,但真到“堆叠布局”这种需要精确定位和层级管理的场景,还是要重新捋一遍规范和习惯。这段时间用Flutter给鸿蒙应用做了一套带图标的按钮、带徽章的图标、…

作者头像 李华
网站建设 2026/10/4 10:53:04

在 Kubernetes 中部署 LiteLLM:用 TaoToken 统一 Key 打通多模型调用

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

作者头像 李华