大整数为什么变成 9007199254740992
结论:超过 2^53-1 的整数在 IEEE 754 双精度里无法精确表示,JSON.parse 会静默把末位改掉。订单号、雪花 ID、金额分单位都踩过这个坑,正确解法是全程当字符串传。

结论:把雪花 ID、订单号、bigint 主键这类标识符当作字符串,从序列化、传输到解析全程不进入数字通道;只有在确认字段不会超过 2^53-1 时,才可以保留数字类型并在 Schema 里加 maximum 约束。
因为解析它的那一刻,精度就已经丢了,而且不会报错。
JSON.parse('{"id": 9007199254740993}');
// { id: 9007199254740992 }
少的那一位,找不回来了。
2^53 这条线怎么来的
JavaScript 的 number 只有一种:IEEE 754 双精度浮点。它用 53 位表示有效数字(1 位隐含 + 52 位显式),所以能精确表示的整数范围是:
-9007199254740991 ~ 9007199254740991 (即 ±(2^53-1))
超过这条线,相邻两个整数之间的距离不再等于 1,而是 2、4、8…… 具体能落到哪个值取决于二进制表示。
9007199254740993 的二进制末位是 1,而在这个量级上能被表示的数末位都是 0,于是它只能进位到 9007199254740992。
哪些字段最容易中招
| 字段 | 典型位数 | 风险 |
|---|---|---|
| 雪花算法 ID | 18–19 位 | 高,末几位直接错 |
| 订单号、流水号 | 16–20 位 | 高 |
| 数据库 bigint 主键 | 19 位 | 高 |
| 金额(以分为单位) | 一般安全 | 低,但换成毫、微就可能超 |
| 时间戳(毫秒) | 13 位 | 安全,能撑到公元 28 万年 |
| 身份证号 | 18 位 | 高,且首位可能被吃掉 |
共同点:它们是标识符,不是用来做算术的数字。标识符不需要加减乘除,只需要原样搬运,所以最合适的类型是字符串。
四种解法
解法一:序列化时就转字符串。 后端在出参前把 bigint 字段转成 string,前端按字符串处理,比较、拼接都不受影响。这是最省事也最可靠的做法,代价是要在接口文档里写清楚「这些字段是字符串」。
解法二:让序列化库处理。 Java 的 Jackson 可以给 Long 类型配 ToStringSerializer;Python 把 int 转 str 再输出;Go 用 json:",string" 标签。核心都是「不让它进数字通道」。
解法三:前端用 BigInt 解析。 JSON.parse 本身不支持,需要 json-bigint 之类的库把大整数解析成 BigInt。注意 BigInt 不能和 Number 混算,JSON.stringify 也不认它,后续处理要配套改。
解法四:约定安全范围。 如果字段确实不会超过 2^53,在 Schema 里加 maximum 约束,让越界数据在校验阶段就失败,而不是等着它在浏览器里被改掉。
怎么验证自己有没有踩坑
拿一个已知的 ID,走一遍「序列化 → 传输 → 解析」,比对前后是否逐字相等。用 JSON 格式化 粘贴原始报文时,注意看数字有没有被高亮成普通数字——超过 16 位的整数就该警觉。
更彻底的办法是抓包看原始响应体:如果响应里是 "id": 1234567890123456789(没有引号),而前端拿到的末位不对,基本可以确定是解析阶段丢的精度。
常见问题
为什么 JSON.parse 改了末位却不报错?
JavaScript 的 number 只有 IEEE 754 双精度一种类型,超出 2^53-1 的整数只能落到附近的近似值上。JSON.parse 把这一步当成正常的数值转换,既不抛错也不提示。
前端能否用 BigInt 保住大整数?
可以,但 JSON.parse 本身不支持,要换成 json-bigint 这类库。代价是 BigInt 不能和 Number 混算,JSON.stringify 也不认它,后续处理要跟着改。
要不要在接口文档里标明这些字段是字符串?
要。序列化时转字符串最省事也最可靠,但前提是消费方能按字符串处理,所以字段类型必须在接口文档里写死,否则前端仍会把它当数字用。