我逐个测了 JSON.stringify 的 7 个坑:第 1 个就在丢数据

javascript 复制代码
const draft = { name: '订单A', remark: undefined, createdAt: new Date('2026-10-10T08:00:00'), cache: new Map([['a', 1]]) };
const back = JSON.parse(JSON.stringify(draft));
// name 在;remark 没了;createdAt 从 Date 变成字符串;cache 变成空对象

JSON.stringify 是前端用得最勤的序列化工具------存缓存、传接口、postMessage、深拷贝兜底,全走它。但它对数据做的动作,比「转成字符串」多不少。我把最容易踩的 7 个坑写成一个实验脚本,27 个用例在 Node 24 里逐个跑了一遍,下面每个输出都是真实结果,代码块可以直接粘进控制台复跑。先看总账:

# 坑 一句话症状
1 键静默消失 undefined、函数、Symbol 的键,序列化后直接没了
2 Date 变字符串 类型丢了,时区还被吃成 UTC
3 NaN 变 null 报表里莫名出现 null,全程没报错
4 Map 变空对象 存进去是 Map,取出来是 {}
5 键顺序重排 正整数键升序插队,依赖顺序的逻辑错乱
6 直接崩 循环引用、BigInt,一个 TypeError 糊脸
7 parse 侧丢数据 重复键静默取后者

坑 1:undefined 的键,序列化后没了

javascript 复制代码
JSON.stringify({ name: '订单A', remark: undefined, cancel: () => {}, [Symbol('id')]: 1 });
// '{"name":"订单A"}'  ← 三个键全没了

典型场景:表单里「备注」没填,remark: form.remark || undefined------提交、存储、再读出来,这个键彻底消失。下游想区分「没有这个字段」和「字段是空的」,依据没了。更阴的是全程不报错,'remark' in obj 是 false,不主动查根本发现不了。

顺带一提,我打印这次实验结果时,结果对象里两个 undefined 值的行直接从输出里消失了------写实验的人被实验坑了一次,这坑的覆盖率是真的。

防御:空值占位用 null,别用 undefined。null 会被保留。

坑 2:Date 变字符串,时区还被吃掉

javascript 复制代码
const d = new Date('2026-10-10T08:00:00');      // 东八区早上 8 点
JSON.stringify({ createdAt: d });
// '{"createdAt":"2026-10-10T00:00:00.000Z"}'  ← 变成 UTC 字符串

const back = JSON.parse(JSON.stringify({ createdAt: d }));
back.createdAt instanceof Date;                  // false,是 string

序列化走 toJSON,toJSON 就是 toISOString,永远吐 UTC。存进去本地 8 点,取回来是 UTC 0 点的字符串,类型也丢了。round-trip 之后所有 instanceof Date 的判断、getTime() 的调用,全部翻车。

我想用 replacer 捞一把 Date,实验泼了冷水:

javascript 复制代码
JSON.stringify({ d: new Date(0) }, (k, v) => {
  console.log(typeof v);  // 'string' ------ replacer 收到时,Date 已被 toJSON 处理过
  return v;
});

toJSON 先于 replacer 执行,等 replacer 接手时已经是字符串了。防御要么给类自定义 toJSON,要么存时间戳,要么别用 JSON 深拷贝带 Date 的对象(见文末 structuredClone)。

坑 3:NaN 和 Infinity,静默变 null

javascript 复制代码
JSON.stringify({ score: NaN, bonus: Infinity, level: -Infinity });
// '{"score":null,"bonus":null,"level":null}'

报表场景:均值算出 NaN(分母为 0),存进缓存再读出来,页面上安静地躺着 null。没人报错,只有用户来问「这里为什么是空的」。防御:序列化前把非有限数显式处理掉,别让 null 替你做决定。

坑 4:Map、Set、RegExp,统统变空对象

javascript 复制代码
JSON.stringify({ cache: new Map([['a', 1]]) });  // '{"cache":{}}'
JSON.stringify({ tags: new Set([1, 2]) });       // '{"tags":{}}'
JSON.stringify({ pattern: /ab+c/gi });            // '{"pattern":{}}'

存的时候明明有数据,取出来长度为 0,遍历不出任何东西。用 JSON 做缓存层,只要对象里混进一个 Map,这部分数据就是单向的------存得进去,取不出来。

防御:传输前 Object.fromEntries(map) 转普通对象,或者别让 JSON 碰这些类型。

坑 5:正整数键会重排,顺序悄悄变

javascript 复制代码
JSON.stringify({ b: 1, '2': 'x', a: 2, '1': 'y' });
// '{"1":"y","2":"x","b":1,"a":2}'  ← 数字键升序排到了前面

V8 里对象键分两种存法:正整数键是「数组索引」,单独存、升序排;字符串键按插入顺序。序列化按内部顺序输出,你定义的顺序就没了。后端按 key 遍历生成下拉选项、按顺序渲染步骤条,顺序悄悄错位。

边界也顺手验证了:负数和浮点键不算数组索引,不会被重排------'-2'、'1.5' 都留在原位,插队的只有正整数键。

防御:顺序是数据的一部分时,用数组,别用对象键。[{id:1},{id:2}] 比 {1:{},2:{}} 稳得多。

坑 6:循环引用和 BigInt,直接抛错

javascript 复制代码
const a = {}; a.self = a;
JSON.stringify(a);
// TypeError: Converting circular structure to JSON

JSON.stringify({ orderId: 1234567890123456789n });
// TypeError: Do not know how to serialize a BigInt

树形结构、双向引用、链表------往接口里一丢,整个请求挂掉。BigInt 这条更值得记住:接口返回的雪花 ID 越来越长,Number 存不下,前端用 BigInt 接住之后再想原样传回去,stringify 直接罢工。

防御:BigInt 过 replacer 转字符串;循环引用要么传输前断环,要么用 WeakSet 标记。

坑 7:parse 这一侧也不干净

javascript 复制代码
JSON.parse('{"amount": 19.99, "amount": 9.99}');
// { amount: 9.99 }  ← 重复键静默取后者

JSON.parse(JSON.stringify(undefined));
// SyntaxError: "undefined" is not valid JSON

重复键不报错、取最后一个------手拼字符串、模板拼接出重复键时,前面的值被悄悄覆盖。第二个更冷:JSON.stringify(undefined) 返回的不是字符串,是 undefined 本身,parse 它直接语法错误。

parse 侧还有条安全细节顺手验证了:注入 {"__proto__": {...}} 在 V8 里不会污染原型(它成为对象的自有属性),但把这个对象 Object.assign 给别的对象,原型就真的被改了;展开运算符 {...obj} 则安全。

速查表:症状 → 根因 → 修复

症状 根因 一行修复
键消失 undefined/函数/Symbol 不序列化 空值用 null 占位
Date 变字符串 toJSON 走 toISOString(UTC) 存时间戳或自定义 toJSON
replacer 捞不到 Date toJSON 先于 replacer 执行 在 toJSON 里动手
NaN 变 null 规范规定非有限数转 null 序列化前显式处理
Map 取出来是 {} Map 没有 toJSON fromEntries 转普通对象
键顺序变了 正整数键是数组索引,升序前置 顺序数据用数组
循环引用崩 无法自引用 WeakSet 断环或 structuredClone
BigInt 崩 规范不支持 replacer 里 v.toString()
重复键覆盖 parse 取最后一个 别手拼 JSON 字符串
assign 后原型被改 proto 走 setter 用展开运算符代替 assign

深拷贝的三个流派

这些坑的公共背景是同一个:JSON 是传输格式,不是拷贝工具。真要做深拷贝,现在有三个流派:

javascript 复制代码
structuredClone({ d: new Date('2026-10-10'), m: new Map([['a', 1]]) });
// Date 还是 Date,Map 还是 Map,循环引用也不崩

structuredClone({ fn: () => {} });
// DOMException: () => {} could not be cloned ------ 函数不支持

structuredClone 救得了坑 2、坑 4、坑 6,救不了函数;JSON round-trip 胜在到处能跑,输在什么都变;lodash 的 cloneDeep 全能,代价是一个依赖。我的立场:进程内拷贝用 structuredClone,跨进程、跨存储才轮到 JSON------别让传输格式兼职拷贝工具。

这 7 个坑的共同点是安静:不报错、不警告、数据默默变形,直到某个下游逻辑用错值才炸。你被第几个坑过、在什么场景发现的,评论区说一声。另外站个队:进程内深拷贝,你选 structuredClone 还是 JSON round-trip------我赌后者的存量比承认的多。

相关推荐
积流成海1 小时前
用代码实现哈希计算器:Web Crypto API 实战
前端·算法·哈希算法·哈希函数·盘子工具站
data analyse 4561 小时前
服务端访问日志聚合 PV 与 UV 怎么实现?日志采集与口径设计
大数据·前端·数据分析
song5011 小时前
React Native for OpenHarmony 实战:三方库 react-native-ratings 的鸿蒙化适配指南
javascript·react native·react.js·分类·harmonyos
JavaGuide1 小时前
DeepSeek Harness 重磅更新,Codex、Claude Code 子智能体进官方插件列表了,还有 Git 工作树和思考翻译!
前端·后端
小羊没烦恼!2 小时前
OS X 上使用.NET开发应用程序
开发语言·前端·数据库·人工智能·c#·策略模式
朱包林2 小时前
iTerm2终端配置指南
linux·javascript·python·云原生·容器·kubernetes·云计算
谢亮_vipxieliang2 小时前
JavaScript this 指向完全指南:调用位置决定一切
开发语言·javascript·ecmascript
Eiceblue3 小时前
使用 Python 实现 Excel 行列转置(保留单元格样式)
前端·python·excel
WeiXin_DZbishe3 小时前
基于Node.js的马拉松赛事管理系统的设计与实现-计算机毕设 附源码【课程设计】90211
java·javascript·vue.js·python·php·课程设计