Javascript is required
踩坑避坑发布于 2026-07-2814 分钟阅读

为什么你的大数字在 JSON 解析后精度丢失了?JavaScript Number 极限解析全解

在日常开发中,订单号、雪花算法 ID(Snowflake ID)、银行卡号或 64 位数据库主键等“看起来只是数字”的字段,一旦传入前端经由 JSON.parse 解析后,经常会出现尾数被悄悄改变的问题。这并非 JSON 格式本身有缺陷,而是 JavaScript 运行时的 Number 类型采用了 IEEE 754 标准造成的。本文将从底层机制到工程落地方案,为你彻底解开这一精度陷阱。 本文将结合生产环境中的真实案例,从底层物理机制、常见避坑陷阱、核心算法实现到工程化最佳实践,本文总结了线上真实排坑经验与落地解决方案。

JSONJavaScriptNumberBigInt精度丢失

一、 精度从哪里开始丢失:IEEE 754 双精度浮点数机制

JavaScript 中的 Number 类型统一采用 IEEE 754 双精度 64 位浮点数格式表示。在这 64 个 bit 中,有 1 位符号位、11 位指数位以及 52 位尾数位(Mantissa/Significand)。由于二进制无法精确表示所有十进制小数,且尾数位数有限,因此它能够无损精确表示的最大连续整数是 2^53 - 1,即 9,007,199,254,740,991 (Number.MAX_SAFE_INTEGER)。

当后端的 64 位整数(如 Java 中的 long、Go 中的 int64)超过这个安全极限时,JavaScript 的解析引擎在将其转换为 Number 对象时就会发生舍入(Rounding)。此时,相邻的多个奇数和偶数可能会被硬性映射到同一个偶数上,导致数据的低位被“抹平”。

需要强调的是,JSON 规范(RFC 8259)本身并没有规定数字的最大长度或精度限制,合法 JSON 允许包含任意长度的数字字面量。因此,服务端返回的 JSON 文本是完全合法的,灾难恰恰发生在前端将其转换为 JS 内存对象的一瞬间。

技术团队应当在持续集成流水线中建立自动化回归测试、代码静态扫描规则与监控预警防线,在研发早期及时捕获并消除边界隐患。

研发人员需要对首屏可交互时间(TTI)、主线程阻塞耗时(TBT)以及高并发负载下的峰值内存占用进行全方位监控。通过

  • 建立完善的自动化回归测试用例,覆盖边界极端场景。
  • 在研发规范中明确字段与接口类型的命名约束,降低沟通成本。
  • 定期审查生产监控日志,及时处置潜在的异常报错。
// 安全整数极限
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991

// 解析超限数字时的精度丢失现象
const rawJson = '{"orderId": 9007199254740993, "snowId": 1819283748593829183}';
const parsed = JSON.parse(rawJson);

console.log(parsed.orderId); // 9007199254740992 (最后一位 3 变成了 2)
console.log(parsed.snowId);  // 1819283748593829000 (低三位全部精度抹平)

二、 前后端交互中的灾难场景:从订单修改失败到删错数据

大数字精度丢失在生产环境中会引发极具隐蔽性且后果严重的 Bug:

1. 业务逻辑错乱:前端展示给用户的订单号末尾变动,用户拿着截屏客服无法查到对应订单;

2. 危险的接口请求:前端从详情页拿到解析后的丢失精度的 ID,再回传给后端进行删除或修改操作(如 DELETE /api/user/1819283748593829000),导致后端错删了数据库中的相邻记录!

技术团队应当在持续集成流水线中建立自动化回归测试、代码静态扫描规则与监控预警防线,在研发早期及时捕获并消除边界隐患。

研发人员需要对首屏可交互时间(TTI)、主线程阻塞耗时(TBT)以及高并发负载下的峰值内存占用进行全方位监控。通过

  • 雪花算法(Snowflake ID):通常为 64 位整数,例如 1819283748593829183,必定超出 JS 安全整数范围。
  • 高频高精金融账单:以“分”或“厘”为单位存储的大额金额计算数值。
  • 分布式链路追踪 TraceID:部分系统使用纯数字作为分布式追踪唯一标识。

三、 三大层面的可靠解决方案

要解决精度丢失问题,核心原则是:不要在 JavaScript 中用 Number 类型来承载“仅仅作为标识符”的大数字。以下是三种不同层面的架构选择:

1. 方案一(推荐最佳实践:契约层转 String):在后端序列化时将 64 位整数统一转为字符串输出。例如 Java 中使用 Jackson 的 @JsonSerialize(using = ToStringSerializer.class) 或 FastJson 的 SerializerFeature.WriteNonStringValueAsString。这样传输到前端的 JSON 变为 {"id": "1819283748593829183"},前端直接按 string 接收,永不失真。

2. 方案二(前端解析拦截:自定义 Reviver 或第三方库):当后端无法配合修改时,前端在接收原始 Response 文本时不能直接调用 JSON.parse。可使用 json-bigint 等第三方库,或者配置包含 BigInt 转换逻辑的解析器,将大整数解析为原生 BigInt 或 BigNumber 实例。

3. 方案三(前端 BigInt 序列化补丁):如果前端使用 BigInt 参与了数值计算,需要注意原生 JSON.stringify(BigInt(123)) 会抛出 TypeError: Do not know how to serialize a BigInt 错误。必须为 BigInt.prototype 补全 toJSON 方法。

技术团队应当在持续集成流水线中建立自动化回归测试、代码静态扫描规则与监控预警防线,在研发早期及时捕获并消除边界隐患。

研发人员需要对首屏可交互时间(TTI)、主线程阻塞耗时(TBT)以及高并发负载下的峰值内存占用进行全方位监控。通过

  • 建立完善的自动化回归测试用例,覆盖边界极端场景。
  • 在研发规范中明确字段与接口类型的命名约束,降低沟通成本。
  • 定期审查生产监控日志,及时处置潜在的异常报错。
// 方案二:使用自定义解析库(以 json-bigint 为例)
import JSONbig from 'json-bigint';

const rawJson = '{"snowId": 1819283748593829183}';
const data = JSONbig({ storeAsString: true }).parse(rawJson);
console.log(data.snowId); // "1819283748593829183" (字符串形式存储,安全无损)

// 方案三:解决 BigInt 的序列化报错
BigInt.prototype.toJSON = function() {
  return this.toString();
};
console.log(JSON.stringify({ id: BigInt("1819283748593829183") })); 
// 输出: '{"id":"1819283748593829183"}'

线上避坑:大 JSON 解析卡顿与边界类型丢精度排查

在前端处理超过 50MB 的超大 JSON 数据时,直接在主线程调用 JSON.parse 会导致页面死锁卡顿几百毫秒甚至崩溃。解决办法是把文本数据交给 Web Worker 在后台子线程解析,解析完成后再把对象以 Transferable Objects 方式传回主线程。

另一个常见的隐蔽 Bug 是 JavaScript 的 64 位双精度浮点数精度限制。超过 9007199254740991 (Number.MAX_SAFE_INTEGER) 的订单 ID 或雪花 ID,在 JSON.parse 时末尾数字会被悄悄改变。对于这种长整数,后端接口必须返回字符串格式,或者前端引入 json-bigint 库进行安全反序列化。

  • 超大 JSON 解析切勿在主线程硬扛,优先使用 Web Worker。
  • 超过 16 位的长整型 ID 必须转为字符串传输,防止精度丢位数。
  • 使用 Try...Catch 包裹 JSON.parse,防止非法语法导致页面白屏。

继续阅读

可直接使用关联工具验证、格式化或检查你的 JSON 数据。

打开工具