先记住这个答案
JSON 的数字没有「任意精度整数」这个概念,所以 JSON.stringify 遇到 BigInt 时选择直接抛 TypeError,而不是偷偷转成 Number。这是故意的:自动转换会让大于 2^53 - 1 的值变成另一个数,序列化看起来成功了,数据却错了。工程上的做法是用 replacer 把 BigInt 转成十进制字符串,在接口约定里写明这个字段是字符串形式的大整数,接收端再按字段名转回 BigInt。解析时不能把所有纯数字字符串都转成 BigInt,手机号、邮编也是纯数字字符串。
- 默认遇到 BigInt 直接抛
TypeError - 转字符串不丢精度,转 Number 会丢
- 转回 BigInt 要按字段约定,不能见数字字符串就转
- 后端返回的超大数字 ID 应该用字符串传
用 replacer 转字符串,再按字段转回来
订单 ID 用 BigInt 存,要发给后端,再从后端取回来。
const order = { id: 9007199254740993n, count: 2 }
try {
JSON.stringify(order)
} catch (err) {
console.log(err.name) // 默认直接报错
}
const json = JSON.stringify(order, (key, value) =>
typeof value === 'bigint' ? value.toString() : value
)
console.log(json)
// 只把约定好的 id 字段转回 BigInt
const back = JSON.parse(json, (key, value) => (key === 'id' ? BigInt(value) : value))
console.log(typeof back.id, back.id === order.id)查看输出与解释
TypeError
{"id":"9007199254740993","count":2}
bigint truereplacer 会对每个属性调用一次,只处理 typeof value === 'bigint' 的值,其他原样返回。JSON 里 id 变成了字符串,所有位数都在。解析时用 reviver,只对 key === 'id' 的值调 BigInt(),往返之后和原值完全相等。
实际项目里字段多了,可以维护一份「哪些字段是 BigInt」的 schema,或者用 zod 这类校验库在解析后统一转换,比在 reviver 里写一堆 if 清楚。
为什么不能转成 Number
Number 是 64 位浮点数,能精确表示的最大整数是 Number.MAX_SAFE_INTEGER,也就是 2^53 - 1。超过它之后,相邻的整数会变成同一个 Number。
const big = 9007199254740993n
console.log(Number(big)) // 末位变了
console.log(Number(big) === Number(big - 1n)) // 两个不同的 ID 变成同一个数
console.log(JSON.parse('{"id":9007199254740993}').id) // 后端返回裸数字也一样
console.log(Number.MAX_SAFE_INTEGER)查看输出与解释
9007199254740992
true
9007199254740992
9007199254740991第二行最危险:两个不同的订单 ID 转成 Number 后相等了,前端拿去查详情,查出来是另一个订单。
第三行是更常见的场景。后端用 Java 的 long 或者雪花算法生成 ID,直接以数字形式写进 JSON。前端 JSON.parse 时就已经按 Number 解析,精度在这一步就丢了,没有任何报错。所以接口约定里,超过安全范围的整数应该一律用字符串传,这要和后端一起定。
- 改
BigInt.prototype.toJSON行不行 - 给
BigInt.prototype加一个toJSON方法,JSON.stringify就不会再报错了。但这是修改全局原型,整个页面所有地方的序列化都会被影响,第三方库、日志上报也都跟着变,出了问题很难查。除非是完全自己控制的应用,否则用局部 replacer 更安全。
容易答错的地方
- 先
Number(bigint)再序列化 - 这只是把报错消掉了,没有保证值正确。超出安全范围后,末位会变,不同的值会撞成同一个。这种 bug 在测试环境 ID 还小的时候完全发现不了。
- 把所有纯数字字符串都转成 BigInt
- reviver 里写
/^\d+$/.test(value) ? BigInt(value) : value,手机号、验证码、邮编都会被转成 BigInt,展示和比较全出问题。转换必须按字段名或 schema 来。
面试官还会怎么问?
能不能让 JSON 里输出成数字而不是字符串?
较新的环境提供了 JSON.rawJSON(),在 replacer 里返回 JSON.rawJSON(value.toString()),输出就是不带引号的数字。解析时 reviver 的第三个参数有 context.source,能拿到原始数字文本再转成 BigInt。这是比较新的能力,目前只有部分环境支持,用之前先查兼容性。另外,接收端如果用普通 JSON.parse,还是会丢精度,所以两端都要配合。
axios 拿到的响应里 ID 已经丢精度了,怎么办?
axios 默认用 JSON.parse 解析响应,到你手里时已经丢了。可以在 transformResponse 里拿到原始文本,用 json-bigint 这类库解析;最根本的办法还是让后端把大整数 ID 以字符串返回。
为什么 JSON.stringify 对 BigInt 报错,对 NaN 却不报错?
NaN 和 Infinity 是规范里早就定好的,统一转成 null。BigInt 是 ES2020 才加的,委员会认为自动转换一定会有人丢精度,所以选择报错,逼调用方显式决定怎么编码。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。