问题场景
某天上线后,运营反馈:"用户详情页打开后,跳转到其他页面时 ID 不对,每次点进去都是另一个人。"
排查发现,后端返回的用户 ID 是这样的:
json
{
"userId": 19028374651234567891,
"name": "张三"
}
前端用 JSON.parse 解析后,userId 的值变成了:
19028374651234567000
最后几位全变了。 跳转时拿这个错误的 ID 去请求,自然查到了另一个人。
最坑的是------这个 Bug 不是必现的。只有 ID 超过一定位数才会触发,小 ID 完全正常,导致开发环境测不出来。
原因分析
JavaScript 的数字限制
JavaScript 中所有数字都遵循 IEEE 754 双精度浮点数标准 (64-bit)。它能安全表示的整数范围是:
diff
- (2⁵³ - 1) ~ 2⁵³ - 1
即 -9007199254740991 ~ 9007199254740991
超过这个范围的整数,位数字太多时,JS 会做舍入处理:
javascript
console.log(19028374651234567891);
// 输出: 19028374651234567000
// ↑ 末尾被截断,精度丢失
你可以用 Number.isSafeInteger() 来验证:
javascript
Number.isSafeInteger(19028374651234567891); // false
JSON.parse 的静默丢失
问题在于 JSON.parse 不会报错------它默默地返回一个精度丢失的数字,没有任何警告:
javascript
const data = JSON.parse('{"id": 19028374651234567891}');
console.log(data.id); // 19028374651234567000
// 完全静默,没有异常,没有 console.warn
这就是为什么这个 Bug 特别难排查:没有报错、没有警告,只是数据不对。
谁的锅?
| 方 | 论点 | 分析 |
|---|---|---|
| 后端 | "我数据库存的明明是完整 ID" | ✅ 数据源没错 |
| 前端 | "我收到的 JSON 字符串也是完整的" | ✅ 传输层没错 |
| 问题 | 出在 JSON.parse 的解析阶段 | 💥 这里炸了 |
谁都不背锅,是 JSON 规范与 JS Number 类型之间天然的鸿沟。
解决方案
方案一:后端改为字符串返回(推荐)
后端将大整数统一以字符串形式传给前端:
json
{
"userId": "19028374651234567891",
"name": "张三"
}
前端无感知,直接当 string 使用。但要注意接口文档明确标注哪些字段是字符串型 ID,避免混用。
方案二:后端改用 Lossless JSON 库(后端侧)
如果后端不方便改类型,可以用支持无损数字的序列化库:
- Java : 使用
Jackson+@JsonSerialize(using = ToStringSerializer.class) - Go : 使用
json:"userId,string"tag - Node.js : 使用
lossless-json库
方案三:前端自己用 reviver 处理(前端侧)
JSON.parse 接受第二个参数 reviver,可以在解析时对特定字段做处理:
javascript
function safeParse(json) {
return JSON.parse(json, (key, value) => {
// 对指定的大数字段做字符串保留
if (key === 'userId' || key === 'id') {
return String(value);
}
return value;
});
}
const data = safeParse('{"userId": 19028374651234567891}');
console.log(data.userId); // "19028374651234567891" ✅
但这个方法有个致命缺陷:reviver 执行时,数字已经被截断了 。所以如果 JSON 字符串里数字本身就超出了安全范围,reviver 拿到的已经是错误的值,String(value) 也救不了。
javascript
// reviver 拿到的是已经被截断的值
JSON.parse('{"id": 19028374651234567891}', (key, val) => {
console.log(val); // 19028374651234567000 ❌ 已丢失精度
return String(val); // "19028374651234567000" ❌ 错的
});
所以方案三只能用于后端已经返回字符串的场景,实际用途有限。
方案四:使用 BigInt + 自定义 JSON 解析
对于确实需要前端解析大整数的场景,可以用 json-bigint 库:
javascript
import JSONbig from 'json-bigint';
const data = JSONbig.parse('{"id": 19028374651234567891}');
console.log(data.id.toString()); // "19028374651234567891" ✅
原理是解析时将大整数转为 BigInt 类型。
方案五:HTTP 层面用 proto3 的 int64 类型(终极方案)
对于 gRPC / protobuf 项目,proto3 的 int64 在 JS 环境中默认被转为 string,天然避坑。
实际项目中的鉴别方法
如何快速知道你的项目有没有这个隐患?
javascript
// 在控制台跑一下
function checkSafeRange() {
const testVal = 9007199254740993;
console.log('原始值:', testVal);
console.log('是否安全:', Number.isSafeInteger(testVal));
console.log('解析后:', JSON.parse(String(testVal)));
}
// 更实用:扫描所有 ID 字段
fetch('/api/user/1')
.then(r => r.json())
.then(data => {
Object.entries(data).forEach(([key, val]) => {
if (typeof val === 'number') {
console.log(key, Number.isSafeInteger(val) ? '✅' : '❌ 精度风险', val);
}
});
});
要点总结
- JS 安全整数范围 是
±2⁵³-1,超过就丢精度 - JSON.parse 静默丢失------不报错、无警告,极难排查
- 推荐方案:后端将大数字 ID 改为字符串格式返回
reviver不顶用------它拿到的已经是截断后的值- Node.js/Go/Java 后端都有对应的序列化方案避免此问题
- 早发现 :在开发阶段就可以用
Number.isSafeInteger给所有数字 ID 做断言
一句话总结 :JSON 里的超长数字在
JSON.parse时会悄无声息地丢失精度。最省心的修法是后端把超过 16 位的数字全换成字符串。别让JSON.parse吃掉你的 ID。