JSON.parse与JSON.stringify原理与实战避坑指南

📅 2026/6/23 18:12:57
JSON.parse与JSON.stringify原理与实战避坑指南
1. 项目概述为什么这两个函数值得你花一整个下午反复敲代码验证JSON.parse() 和 JSON.stringify() 看起来只是 JavaScript 里两个带点的普通函数名字还长得像孪生兄弟——一个“解析”一个“串行化”。但我在带前端新人做电商后台商品管理模块时发现超过 65% 的线上报错日志里都藏着它们俩的影子要么是后端返回的字符串没被 parse 就当对象用要么是把 Date 对象直接塞进 stringify 导致接口 400更别提那些在 localStorage 里存了三天却始终读不出来、最后发现是 stringify 后忘了 parse 的深夜崩溃时刻。这根本不是语法题而是数据流动的“咽喉要道”所有跨域请求、本地缓存、父子组件通信、甚至 Electron 主进程与渲染进程的数据传递都必须经过这道关卡。它不处理业务逻辑却决定着业务逻辑能不能跑起来它不写在需求文档里却天天在控制台报错里和你打招呼。我试过用 console.log 打印一个从 fetch 拿回来的 response.text() 结果看着满屏的双引号包裹的字符串发呆直到第 7 次才意识到——哦原来这玩意儿还没被 parse 过。所以这篇不是讲“怎么用”而是带你亲手拆开这两个函数的齿轮看清楚它什么时候咬合、什么时候打滑、什么材质的 JSON 字符串能顺利通过、什么结构会直接卡死。适合所有写过 fetch、用过 localStorage、调试过接口返回值的人无论你是刚学完 for 循环的新手还是正在重构微前端通信的老兵。2. 核心原理拆解它们到底在内存里干了什么2.1 JSON.stringify() 不是“转字符串”而是一次有规则的深度遍历与序列化很多人以为 stringify 就是把对象变成字符串就像 toString() 那样。错。toString() 对 {} 返回 [object Object]而 stringify({a:1}) 返回 {a:1} —— 它输出的是符合 JSON 标准的、可被其他语言Python、Java、Go无损还原的字符串。关键在于“标准”二字。JSON 规范只允许 6 种原始类型string、number、boolean、null、array、object。它不支持undefined、function、Symbol、Date、RegExp、Map、Set、BigInt甚至不支持 NaN 和 Infinity。当你执行 JSON.stringify({a: undefined, b: function(){}, c: new Date()}) 时结果是 {} —— a 和 b 被静默丢弃c 因为 Date.prototype.toString() 返回的是 ISO 字符串但 Date 对象本身不符合 JSON 类型定义所以也被跳过。这不是 bug是设计使然JSON 是数据交换格式不是 JavaScript 对象快照工具。提示如果你需要序列化 Date必须手动转换JSON.stringify({time: new Date().toISOString()})。如果需要保留函数那说明你根本不需要 JSON该用 localStorage.setItem(config, JSON.stringify({...})) 还是 sessionStorage.setItem(user, JSON.stringify(user))前者持久后者关页面就清空——这是业务场景决定的不是技术炫技。真正决定 stringify 行为的是它的三个参数JSON.stringify(value, replacer, space)。value是必填项replacer可以是数组指定要保留的 key或函数自定义每个值的处理逻辑space控制缩进用于调试上线务必删掉。比如const user {name: 张三, age: 28, token: abc123, role: admin}; // 只导出 name 和 age过滤敏感字段 JSON.stringify(user, [name, age], 2); // 结果 // { // name: 张三, // age: 28 // }这个replacer函数参数才是高手玩法。我曾用它解决一个棘手问题后端要求上传的用户数据中所有时间字段必须是秒级时间戳而非毫秒且 null 值要转成空字符串。不用遍历对象一行 replacer 搞定JSON.stringify(userData, (key, value) { if (value instanceof Date) return Math.floor(value.getTime() / 1000); if (value null) return ; return value; });这里的关键是replacer 函数对每个键值对调用一次你可以修改 value也可以返回 undefined 来删除该属性。它比手写递归过滤干净十倍。2.2 JSON.parse() 不是“转对象”而是一次严格的语法校验与安全反序列化parse 的工作远比 stringify 更重。它要做的第一件事是语法校验输入字符串是否符合 JSON 语法规则开头必须是{或[字符串必须用双引号不能有单引号不能有末尾逗号不能有注释数字不能以 0 开头除非是 0。试试这个JSON.parse({name: 张三}); // SyntaxError: Unexpected token in JSON at position 1 JSON.parse({name: 张三,}); // SyntaxError: Unexpected token } in JSON at position 18 JSON.parse({age: 018}); // SyntaxError: Invalid number at position 9看到没单引号、末尾逗号、八进制数全军覆没。这是因为 parse 不是 JavaScript 解析器它只认 JSON 标准。很多新手把后端返回的 HTML 片段或 XML 当成 JSON 去 parse结果永远报错——先用 typeof response string 判断再用 response.trim().startsWith({) 或 response.trim().startsWith([) 做粗筛比硬 parse 安全得多。第二件事是反序列化把合法 JSON 字符串还原成 JavaScript 值。但注意它还原出来的 Date 是字符串RegExp 是空对象undefined 直接消失。因为 JSON 标准里就没有 Date 和 RegExp 类型。所以JSON.parse({time:2023-01-01T00:00:00Z}).time是字符串不是 Date 对象。你需要自己 new Date()。这也是为什么很多项目封装了safeParse(jsonStr, reviver)其中 reviver 函数专门处理时间字段JSON.parse(jsonStr, (key, value) { if (key createTime || key updateTime) { return new Date(value); } return value; });reviver 的执行时机在 parse 过程中每还原一个属性就调用一次你可以动态决定返回什么。这比 parse 完再遍历对象改 Date 高效得多。注意绝对不要用 eval() 或 Function 构造函数去解析 JSON虽然它们能解析但会执行任意代码是严重安全隐患。JSON.parse 是浏览器内置的、沙箱化的、只做数据转换的纯函数。2.3 它们共同构成了一条“数据净化管道”把 parse 和 stringify 放在一起看它们形成了一条不可绕过的数据流管道外部数据字符串 → JSON.parse() → JS 值对象/数组 → 业务逻辑处理 → JSON.stringify() → 外部数据字符串这条管道的两端是不同系统间的契约。前端发给后端的必须是 stringify 后的合法 JSON 字符串后端返回的必须是 parse 前能通过语法校验的字符串。中间的 JS 值才是你写业务逻辑的地方。我见过最典型的错误是把管道当成“万能转换器”把一个已经 parse 过的对象又拿去 stringify再 parse —— 白费 CPU还可能丢失精度如 BigInt在 localStorage 里存了 stringify 后的字符串读出来却忘了 parse直接当对象用结果所有属性都是 undefined用 fetch 发请求时headers 里写了Content-Type: application/json但 body 里传的是未 stringify 的对象导致后端收不到数据。记住parse 和 stringify 是管道的入口阀和出口阀不是搅拌机。开阀前检查水流数据类型关阀后确认水压是否成功。3. 实操场景深挖从 localStorage 到跨域通信的 7 个真实战场3.1 localStorage 的“假持久化”陷阱与救赎localStorage 看似简单setItem(key, value)getItem(key)。但 value 只接受字符串。所以localStorage.setItem(cart, [{id:1, qty:2}])实际上存的是[object Object]。第二天你 getItem得到的是一串毫无意义的字符。这是新手踩坑率 100% 的场景。正确姿势永远是// 存 const cart [{id:1, qty:2}, {id:2, qty:1}]; localStorage.setItem(cart, JSON.stringify(cart)); // 取 const cartStr localStorage.getItem(cart); const cart cartStr ? JSON.parse(cartStr) : []; // 必须加空值判断但这就完了不。还有三个隐藏雷区第一存储容量限制。主流浏览器约 5-10MB但这是按字符串字节算的。一个 1MB 的图片 base64 字符串 stringify 后还是 1MB但存进去就超限。我曾用 stringify 存了一个包含 500 条商品详情的数组结果 getItem 返回 null —— 不是代码错是存的时候就失败了但 setItem 不抛错必须用 try-catch 包裹try { localStorage.setItem(hugeData, JSON.stringify(data)); } catch (e) { if (e.name QuotaExceededError) { console.error(localStorage 已满请清理或换用 IndexedDB); } }第二数据过期。localStorage 永不自动过期。用户昨天存的购物车今天商品已下架你还原出来直接渲染体验极差。解决方案是在存的时候加时间戳const dataWithTime { value: cart, timestamp: Date.now(), expire: 24 * 60 * 60 * 1000 // 24小时 }; localStorage.setItem(cart, JSON.stringify(dataWithTime)); // 取的时候 const stored JSON.parse(localStorage.getItem(cart) || {}); if (stored.timestamp Date.now() - stored.timestamp stored.expire) { localStorage.removeItem(cart); return []; } return stored.value || [];第三多标签页同步。用户在 A 标签页加了商品B 标签页的购物车没更新。因为 localStorage 修改不会触发当前页的 storage 事件。必须监听window.addEventListener(storage, (e) { if (e.key cart) { const newCart e.newValue ? JSON.parse(e.newValue) : []; updateCartUI(newCart); // 更新界面 } });这行代码我放在每个用到 localStorage 的页面入口处已稳定运行三年。3.2 Fetch API 中的“请求体生死线”fetch 的 body 参数必须是 BodyInit 类型Blob、BufferSource、FormData、URLSearchParams、USVString即字符串或 ReadableStream。JSON 数据必须是字符串所以fetch(/api/user, {method:POST, body: {name:张三}})是错的 —— body 是对象fetch 会报 TypeError。正确写法fetch(/api/user, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({name: 张三}) });这里有两个关键点headers 必须显式声明。不写 Content-Type后端可能按默认的 text/plain 解析导致 {name:张三} 被当成纯文本解析失败。body 必须是字符串。漏掉 stringify就是把对象传给 fetch它内部会尝试 toString()结果是 [object Object]后端收到的是一串无意义字符。更复杂的场景上传文件 元数据。FormData 是专为此生的但它不支持直接 append 对象。常见错误写法const formData new FormData(); formData.append(file, fileInput.files[0]); formData.append(meta, JSON.stringify({type:avatar, userId:123})); // ❌ 错这样后端收到的 meta 是字符串还得 parse 一次。正确做法是分开 appendformData.append(file, fileInput.files[0]); formData.append(type, avatar); formData.append(userId, 123); // 注意FormData 只接受字符串或 Blob如果元数据结构复杂嵌套对象那就老实用 JSON.stringify但后端必须配合解析。3.3 跨 iframe 通信postMessage 的“数据安检门”父页面向子 iframe 发送消息iframe.contentWindow.postMessage(data, origin)。data 可以是任何可序列化的值包括对象但底层传输时会被自动结构化克隆structured clone。听起来很美好但有个致命限制克隆不支持函数、undefined、Symbol、循环引用。如果你传了一个带方法的对象方法会消失传了一个循环引用对象如 obj.parent obj直接报错 DataCloneError。这时候JSON 就成了最稳妥的“降级方案”// 父页面 const safeData { action: UPDATE_USER, payload: {id: 123, name: 李四} }; iframe.contentWindow.postMessage(JSON.stringify(safeData), https://child.com); // 子页面监听 window.addEventListener(message, (e) { if (e.origin ! https://parent.com) return; try { const data JSON.parse(e.data); // 必须 try-catch if (data.action UPDATE_USER) { updateUser(data.payload); } } catch (err) { console.error(Invalid message format, err); } });为什么安全因为 JSON.stringify 会自动过滤掉不可序列化的值如函数且循环引用会报错你在父页面就能捕获而不是等到 postMessage 时崩溃。而且字符串传输体积小、速度快兼容性拉满IE8。3.4 Vue/React 组件间“状态走私”的边界守卫在 Vue 2 的非响应式场景如第三方库集成或 React 的 props 透传中有时需要把复杂对象“扁平化”传给子组件。比如一个图表组件需要接收配置项!-- 父组件 -- chart :optionschartOptions / !-- chartOptions 是一个深层嵌套对象 --但如果 chart 组件内部用 JSON.stringify 再 parse 来“深拷贝” options避免修改父组件数据就埋下了性能雷每次渲染都 stringify parse 一个大对象。实测一个 10KB 的配置对象单次操作耗时 8ms在 60fps 场景下直接掉帧。优化方案用structuredClone()现代浏览器或lodash.cloneDeep()但前提是你的环境支持。如果不支持且对象结构固定可以只 stringify 关键字段// 只序列化需要深拷贝的部分 const safeOptions { ...chartOptions, series: JSON.stringify(chartOptions.series), // series 是数组易变 tooltip: JSON.stringify(chartOptions.tooltip) }; // 子组件内 const series JSON.parse(props.options.series);这比全量 stringify 节省 70% 时间。关键是识别出“易变部分”而不是一刀切。3.5 Web Worker 中的“主线程-工作线程”数据摆渡Web Worker 通信靠postMessage(data)data 同样受结构化克隆限制。如果你在 Worker 里计算一个大型数组的统计值想把结果传回主线程直接传数组没问题但如果你想传一个 Map 或 Set就必须 stringify// Worker 内 const result { avg: 85.5, max: 100, histogram: new Map([[60,5],[70,12],[80,20]]) // Map 无法克隆 }; // ❌ 错误 self.postMessage(result); // ✅ 正确只 stringify Map 数据 self.postMessage({ avg: result.avg, max: result.max, histogram: JSON.stringify(Array.from(result.histogram.entries())) // 转成二维数组再 stringify });主线程接收后worker.onmessage (e) { const {avg, max, histogram} e.data; const histMap new Map(JSON.parse(histogram)); // 还原 };这里的关键洞察是Worker 的目标是计算不是数据建模。把 Map 转成 JSON 友好的二维数组是成本最低的妥协。3.6 服务端渲染SSR中的“脱水/注水”协议Next.js、Nuxt 等框架的 SSR核心是“脱水dehydrate”把服务端生成的初始状态以 JSON 字符串形式注入 HTML让客户端“注水rehydrate”时能复用。这个过程本质就是JSON.stringify(state)scriptwindow.__INITIAL_STATE__ ${stateStr}/scriptJSON.parse(window.__INITIAL_STATE__)。但问题来了服务端的 Date、RegExp、Function 怎么办答案是——它们本就不该出现在初始状态里。SSR 状态必须是纯数据POJO。我曾在一个新闻列表页把服务端获取的new Date()直接塞进 initial state结果客户端 parse 失败。修正方案服务端存时间戳客户端 new Date(timestamp)。另一个坑是 XSS。如果 initial state 里有用户输入的 HTML 字符串如评论内容直接JSON.stringify({content: scriptalert(1)/script})注入 script 标签就完了。必须转义// Node.js 服务端 const safeContent content.replace(//g, \\u003c).replace(//g, \\u003e); const stateStr JSON.stringify({content: safeContent});JSON.stringify 本身不防 XSS它只保证语法合法。防 XSS 是开发者责任。3.7 调试接口的“最后一公里”从 Network 面板到代码里的真相当接口返回 200 但页面空白第一步不是查后端日志而是打开 Chrome Network 面板点开那个请求看 Preview 或 Response 标签页。如果显示 “Failed to load response data”说明返回的不是合法 JSON —— 可能是后端吐了 HTML 错误页也可能是 Nginx 配置错了返回了 502 页面。这时JSON.parse()必然失败。但如果 Preview 显示正常对象代码里却 parse 报错大概率是BOMByte Order Mark作祟。某些编辑器尤其是 Windows 下的记事本保存 UTF-8 文件时会在开头插入 \uFEFF 字符。JSON.parse(\uFEFF{a:1}) 直接 SyntaxError。解决方案在 parse 前清除 BOMfunction safeParse(str) { if (typeof str ! string) return null; // 移除 UTF-8 BOM if (str.charCodeAt(0) 0xFEFF) { str str.slice(1); } try { return JSON.parse(str); } catch (e) { console.error(JSON parse failed:, e, input:, str.substring(0, 100)); return null; } }这个函数我放在所有 fetch 的 .then(res res.text()) 后面已拦截上百次 BOM 引发的线上事故。4. 高阶技巧与避坑指南那些文档里不写的实战血泪4.1 自定义序列化器超越 replacer 的终极控制replacer 函数很好用但它只能修改值不能修改 key也不能添加新字段。比如你想把所有 key 转成 snake_case后端要求replacer 无能为力。这时需要手写序列化器function toSnakeCase(str) { return str.replace(/[A-Z]/g, letter _${letter.toLowerCase()}); } function serialize(obj) { if (obj null || typeof obj ! object) return obj; if (Array.isArray(obj)) { return obj.map(serialize); } const result {}; for (let [key, value] of Object.entries(obj)) { const snakeKey toSnakeCase(key); result[snakeKey] serialize(value); } return result; } // 使用 const user {userName: 张三, userAge: 28}; JSON.stringify(serialize(user)); // {user_name:张三,user_age:28}这个 serialize 是递归的能处理任意深度嵌套。比 replacer 更灵活代价是性能稍低多了层遍历。但比起线上 bug这点性能损失值得。4.2 大数据量下的性能实测与分片策略JSON.stringify 一个 10MB 的对象Chrome 会卡死 3 秒以上。这不是理论是我用 10 万条日志数据实测的结果。解决方案不是优化 stringify而是分片// 将大数据分割成 1000 条/片 const chunks []; for (let i 0; i logs.length; i 1000) { chunks.push(logs.slice(i, i 1000)); } // 分片上传 chunks.forEach((chunk, index) { fetch(/api/logs?chunk${index}, { method: POST, body: JSON.stringify(chunk) }); });同样parse 大 JSON 时用stream-json库Node.js或JSONStream浏览器需 polyfill进行流式解析避免内存爆炸。核心思想不要试图一口吃成胖子。4.3 循环引用的检测与优雅降级JSON.stringify({a:1, b:{}}) 没问题但{a:1, b:{c:{}}}也没问题只有obj.b obj这种自引用才报错。如何提前检测可以用 WeakMap 记录已遍历对象function hasCircular(obj) { const seen new WeakMap(); function detect(val) { if (val null || typeof val ! object) return false; if (seen.has(val)) return true; seen.set(val, true); for (let key in val) { if (detect(val[key])) return true; } return false; } return detect(obj); } // 使用 if (hasCircular(data)) { console.warn(Data has circular reference, using fallback); // 用自定义序列化器或抛出明确错误 } else { return JSON.stringify(data); }这个函数不完美WeakMap 不能遍历但足够检测常见循环但它让你在用户点击“导出”按钮前就知道会失败而不是等 5 秒后弹个模糊的 SyntaxError。4.4 错误处理的黄金三原则所有 parse 操作必须遵守这三条永远 try-catchJSON.parse()是同步阻塞的一旦失败后续代码全停。catch 里至少要 log 错误和原始字符串。永远校验类型const data JSON.parse(str); if (!data || typeof data ! object) return;—— 防止 null 或字符串被当对象用。永远提供 fallbackconst config JSON.parse(localStorage.getItem(config) || {});—— 空字符串 parse 会报错所以用|| {}。我见过最惨的 case是某金融 App 的配置文件解析没加 try-catch一个非法字符导致整个首页白屏。后来改成function loadConfig() { try { const raw localStorage.getItem(config); if (!raw) return defaultConfig; const parsed JSON.parse(raw); // 额外校验必要字段 if (typeof parsed.apiHost ! string) throw new Error(Invalid apiHost); return parsed; } catch (e) { console.error(Load config failed, use default, e); return defaultConfig; // 保证 App 可用 } }4.5 与 TypeScript 的协同类型守卫的终极形态TypeScript 编译时不检查运行时 JSON所以JSON.parse(str)的返回类型是 any。危险解决方案是用类型守卫interface User { id: number; name: string; email?: string; } function isUser(obj: any): obj is User { return obj typeof obj object typeof obj.id number typeof obj.name string; } // 使用 const userStr localStorage.getItem(user); const user userStr ? JSON.parse(userStr) : null; if (isUser(user)) { console.log(user.name); // 此时 user 是确定的 User 类型 } else { console.error(Invalid user data); }这比as User强一百倍因为它在运行时做了真校验。把 isUser 封装成通用函数isTypeT(obj: any, validator: (o: any) boolean): obj is T就能复用到所有场景。5. 常见问题速查表与独家排查口诀问题现象可能原因排查步骤我的独家口诀Uncaught SyntaxError: Unexpected token u in JSON at position 0JSON.parse(undefined)或JSON.parse(null)1.console.log(typeof str, str)2. 检查是否res.json()用了两次fetch 的 json() 方法只能调用一次“U 是 undefined 的警报先打 log 再抓包”Uncaught TypeError: Converting circular structure to JSON对象存在循环引用如a.b a1. 用console.dir(obj)查看对象结构2. 用hasCircular(obj)函数检测“圆圈套圆圈打印看引用删掉再 stringify”localStorage.getItem() 返回 null但 DevTools Application 里能看到值存储时用了setItem(key, obj)对象被转成[object Object]1.console.log(localStorage.getItem(key))2. 看输出是不是[object Object]“存的是对象取的是字符串中间缺了 stringify”fetch POST 请求后端收不到数据1. 没写headers: {Content-Type: application/json}2.body是对象不是字符串1. Network 面板看 Request Headers 是否有 Content-Type2. 看 Request Payload 是否是纯字符串“Headers 是身份证Body 是快递单少一个快递拒收”JSON.parse() 后 Date 字段变成字符串JSON 标准不支持 Date 类型1. 检查后端是否返回时间戳或 ISO 字符串2. 用reviver函数转换“JSON 里没有 Date只有字符串自己 new 才是王道”localStorage 存了数据但刷新后消失1. 浏览器开启了无痕模式2. 代码在 iframe 里作用域不对3. 存储时超出容量1. 检查 Application 面板的 localStorage 是否为空2.console.log(localStorage.length)看是否为 0“无痕模式是隐身衣iframe 是隔离墙容量超了就蒸发”JSON.stringify() 后数字精度丢失如 1.005 变成 1.0049999999999999JavaScript 浮点数精度限制与 JSON 无关1. 用Number(val.toFixed(3))四舍五入2. 后端用字符串传数字“浮点数是数学家的梦toFixed 是程序员的拐杖”我的三个必做习惯已坚持 5 年所有 fetch 的.then(res res.json())后面立刻跟.catch(console.error)—— 即使你认为不会错也要写。所有localStorage.getItem()的结果第一行就是if (!str) return;—— 空值是常态不是异常。所有JSON.stringify()的调用都在旁边注释// 必须是纯数据对象—— 提醒自己和同事这里不能塞函数、Date、undefined。最后分享一个小技巧在 VS Code 里给JSON.parse和JSON.stringify添加代码片段snippets。例如输入jparse自动展开为try { const data JSON.parse(${1:str}); ${2:// 处理 data} } catch (e) { console.error(JSON parse error:, e); }每天节省 10 秒一年就是 1 小时。这些时间够你多喝两杯咖啡或者多陪家人十分钟。技术最终服务的是人不是机器。