JSON.stringify大概是前端用得最多、也最少被怀疑的函数之一。它的问题不在于会报错而在于大部分时候不报错只是悄悄把数据改了字段没了、类型变了、精度丢了等你在后端日志里发现的时候已经不知道是哪一步出的事。下面八个地方都在 Node 25.8 里实跑过输出原样贴出来浏览器里行为一致。1. undefined、函数、Symbol在对象里消失在数组里变 nullJSON.stringify({a:undefined,b:()1,c:Symbol(x),d:null});// {d:null}JSON.stringify([undefined,()1,Symbol(x)]);// [null,null,null]同样的值放在对象里是整个键被删掉放在数组里变成null因为数组要保住下标。实际踩坑的场景表单里某个选填项没填值是undefined提交后后端根本收不到这个字段而不是收到一个空值。如果后端的逻辑是「字段不存在就不更新」用户想清空某个字段就永远清不掉。要表达「清空」前端得显式传null。2. NaN 和 Infinity 变成 nullJSON.stringify({n:NaN,i:Infinity,m:-0});// {n:null,i:null,m:0}JSON 规范里没有 NaN 和 Infinity于是统一变成null-0变成0。计算结果除了个零得到 Infinity传到后端就成了 null后端再按「没有数据」处理报表上看到的是一个空格而不是一个错误。建议在序列化前对数值做一次校验Number.isFinite(x)不通过就当成错误处理别让它悄悄过去。3. Date 出去是字符串回来还是字符串constdnewDate(2026-09-30T10:00:0008:00);JSON.stringify({d});// {d:2026-09-30T02:00:00.000Z}constbackJSON.parse(JSON.stringify({d}));typeofback.d;// stringback.dinstanceofDate;// false两个问题叠在一起一是 Date 会被转成 UTC 的 ISO 字符串东八区上午 10 点变成了02:00:00.000Z不熟悉的人会以为时间错了 8 小时二是JSON.parse不会自动还原成 Date之后调back.d.getTime()直接报错。需要还原时用 reviverJSON.parse(text,(k,v)(kd?newDate(v):v)).dinstanceofDate;// true4. BigInt 直接抛错大整数悄悄丢精度JSON.stringify({id:12345678901234567890n});// TypeError: Do not know how to serialize a BigIntJSON.parse({id: 12345678901234567890}).id;// 12345678901234567000第一个至少会报错。第二个更危险后端返回的 19 位订单号、雪花 ID超过Number.MAX_SAFE_INTEGER2^53 - 1之后JSON.parse会把它变成最接近的 double末尾几位直接变成 0。拿这个 ID 再去查详情查到的是另一条数据或者查不到。最稳的做法是跟后端约定超过 2^53 的 ID 一律用字符串传。如果改不了接口较新的运行时可以用JSON.parse的第三个参数拿到原始文本Node 25.8 实测可用JSON.parse({id: 12345678901234567890},(k,v,ctx)(kid?ctx?.source:v)).id;// 12345678901234567890序列化方向也有对应的JSON.rawJSONJSON.stringify({id:JSON.rawJSON(12345678901234567890)});// {id:12345678901234567890}这两个 API 比较新要兼容旧浏览器的项目上线前先确认目标环境是否支持。5. 循环引用直接报错consto{name:a};o.selfo;JSON.stringify(o);// TypeError: Converting circular structure to JSON常见于把 DOM 节点、Vue/React 的响应式对象、带 parent 指针的树直接丢进去打日志。打日志时可以用 replacer 去环constseennewWeakSet();JSON.stringify(o,(k,v){if(typeofvobjectv!null){if(seen.has(v))return[Circular];seen.add(v);}returnv;});// {name:a,self:[Circular]}注意这个写法有个副作用同一个对象如果在两个地方被引用并不是循环第二次出现也会被标成[Circular]。打日志够用做数据传输就不合适了。6. Map 和 Set 变成空对象JSON.stringify({m:newMap([[a,1]]),s:newSet([1,2])});// {m:{},s:{}}不报错数据全没了。这可能是最隐蔽的一个因为{}看起来像是「本来就是空的」。需要的话用 replacer 显式转换JSON.stringify(data,(k,v)vinstanceofMap?Object.fromEntries(v):vinstanceofSet?[...v]:v);// {m:{a:1},s:[1,2]}7. toJSON 会接管输出JSON 深拷贝顺带把上面全踩一遍对象上有toJSON方法时序列化的是它的返回值classMoney{constructor(c){this.centsc;}toJSON(){return(this.cents/100).toFixed(2);}}JSON.stringify({price:newMoney(1990)});// {price:19.90}这是个有用的扩展点Date 本身就是靠toJSON变成字符串的。但也意味着你看到的 JSON 未必是对象的真实结构排查问题时要记得看一眼有没有toJSON。很多人用JSON.parse(JSON.stringify(x))做深拷贝它会把前面几个坑一次性全踩一遍constsrc{d,u:undefined,m:newMap([[a,1]])};JSON.parse(JSON.stringify(src));// { d: 2026-09-30T02:00:00.000Z, m: {} } u 没了d 成了字符串m 空了constcstructuredClone(src);c.dinstanceofDate;// trueuinc;// truec.minstanceofMap;// true现在的浏览器和 Node 都有structuredClone深拷贝应该优先用它。它也有局限函数、DOM 节点、类实例的原型链都复制不了但至少不会悄悄丢数据复制不了的会直接报错。8. 数字键会被排到最前面JSON.stringify({b:1,2:x,a:2,1:y});// {1:y,2:x,b:1,a:2}这不是 JSON 的问题是 JS 对象属性顺序的规则整数形式的键按数值升序排在最前其余字符串键按插入顺序。如果你拿 JSON 字符串做签名、做缓存 key、或者做两段数据的文本 diff顺序不同就会被当成不同的数据。这类场景要么先对键排序再序列化要么比较的时候按结构比不按文本比。一张速查表输入输出会报错吗对象里的 undefined / 函数 / Symbol键被删掉不会数组里的 undefined / 函数 / Symbolnull不会NaN / Infinitynull不会DateUTC ISO 字符串parse 回来仍是字符串不会BigInt—会超过 2^53 的整数parse 时精度丢失不会循环引用—会Map / Set{}不会会报错的只有两行剩下六行全是静默的。我平时核对接口数据时会把返回的 JSON 贴到格式化工具里看结构比如 forxi.cn 上的 JSON 格式化。但要说清楚任何格式化工具看到的都已经是序列化之后的文本上面这些丢失发生在序列化那一步格式化工具帮不了你发现只能在代码里提前处理。最实用的一条习惯是凡是跨边界发请求、存 localStorage、postMessage的数据序列化前后各打一次日志对一下比事后排查省事得多。