先记住这个答案
JSON 的数字只能是有限的十进制数,语法里没有 NaN、Infinity 和 -Infinity。JSON.stringify 遇到这三个值时不会输出非法的文本,也不会抛错,而是统一写成 null,不管它是顶层值、对象属性还是数组元素。这样生成的 JSON 一定合法,但信息丢了:解析回来只剩 null,和原本就是 null 的值也分不清。所以要在序列化之前处理,发送前用 Number.isFinite 校验;如果特殊值有业务含义,就用 replacer 编码、reviver 解码。
NaN、Infinity和-Infinity都会变成null- 不报错,所以数据丢了也很难发现
- 提交前用
Number.isFinite校验 - 确实要保留,就用 replacer 和 reviver 成对编码
先看序列化后的真实文本
三种位置都试一下,再做一次往返。
nullJavaScriptconsole.log(JSON.stringify(NaN))
console.log(JSON.stringify({ high: Infinity, low: -Infinity }))
console.log(JSON.stringify([1, NaN, Infinity]))
const restored = JSON.parse(JSON.stringify({ value: NaN }))
console.log(restored.value, Number.isNaN(restored.value)) // 回不去了查看输出与解释
null
{"high":null,"low":null}
[1,null,null]
null false代码全部正常跑完,一个报错都没有,这正是这个问题难发现的原因。对象里的属性没有被删掉,而是保留了键、值变成 null;数组里也保留了位置。这和 undefined 的处理不同,undefined 在对象里会让整个属性消失。
最后一行说明往返之后拿到的是 null,不是 NaN。如果 NaN 表示「计算失败」、Infinity 表示「没有上限」,默认序列化就把这些不同的业务状态压成了同一个值。
- 为什么选
null,而不是报错或转字符串 - JSON 是跨语言的数据格式,很多语言的解析器不接受
NaN这样的写法,直接输出会生成非法 JSON。转成字符串"NaN"又改变了类型,接收方拿到一个字符串,可能直接拿去显示。null是 JSON 里唯一能表示「这里没有有效值」的东西,所以规范选了它。
发送前拦住,或者显式编码
金额、坐标、统计指标这类字段,出现 NaN 或 Infinity 基本都是 bug。最稳的做法是提交前用 Number.isFinite(value) 校验,不合法就报错,把问题留在发生的地方。
如果特殊值确实有业务含义,比如「无上限」,就要约定一种编码方式,发送端和接收端成对实现。
const encode = (key, value) =>
typeof value === 'number' && !Number.isFinite(value) ? { $num: String(value) } : value
const decode = (key, value) =>
value && typeof value === 'object' && '$num' in value ? Number(value.$num) : value
const data = { a: NaN, b: Infinity, c: -Infinity, d: 1 }
const json = JSON.stringify(data, encode)
console.log(json)
const back = JSON.parse(json, decode)
console.log(back.a, back.b, back.c, back.d)查看输出与解释
{"a":{"$num":"NaN"},"b":{"$num":"Infinity"},"c":{"$num":"-Infinity"},"d":1}
NaN Infinity -Infinity 1replacer 在序列化每个值之前调用,所以能在 NaN 被转成 null 之前把它换掉。String(NaN) 是 'NaN',而 Number('NaN') 又能转回 NaN,对 Infinity 也同理。reviver 从里到外处理,看到带 $num 标记的对象就还原成数字。
这种编码只有两端都认识才有意义。如果只是「没有上限」,更好的做法是在接口里设计一个明确的字段,比如 maxLimit: null 配合 unlimited: true,而不是传一个 JavaScript 特有的值。
容易答错的地方
- 以为
JSON.stringify会抛错 - BigInt 和循环引用会抛
TypeError,但NaN和Infinity不会,它们静默变成null。排查数据丢失时要看输出内容,光靠try/catch抓不到。 - 用全局
isFinite做校验 - 全局
isFinite会先把参数转成数字,isFinite('')和isFinite(null)都是true,因为它们会被转成 0。接口校验要用Number.isFinite(value),它不做类型转换,只有真正的有限数字才返回true。
面试官还会怎么问?
JSON.parse 能把 null 自动恢复成 NaN 吗?
不能。文本里只剩 null,它可能原本就是 null,也可能来自三个特殊值中的任何一个,已经没有区分的依据。只有序列化时额外编码了类型信息,解析时才能恢复。
为什么 BigInt 报错,NaN 却不报错?
NaN 转 null 是 JSON 最早的规范就定下的行为,改了会破坏大量现有代码。BigInt 是后来加入的类型,委员会认为自动转换一定会丢精度,所以选择报错,逼调用方显式决定怎么编码。
从哪里最容易冒出 NaN?
最常见的是解析失败,比如 Number('abc') 和 parseInt('');其次是对空数组求平均时 0 除以 0;还有读取了不存在的字段再做运算,undefined + 1 就是 NaN。表单提交前对数字字段统一做一次 Number.isFinite 检查,能拦住绝大部分。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。