接口发了 775692479844696084,解析出来却少了 40 位——今天 ES2026 用两个 API 把 JSON 精度这件事从根上修好了

写前端的都踩过这个坑:从后端接了一个 orderId: 775692479844696084,一解析就变成 775692479844696000,差了几十位。一路追到后端,后端说「我发的是对的」。这个坑不是 bug,是 JavaScript Number 类型和 JSON 数字的天然局限——JavaScript 的双精度浮点存不下所有 64 位整数,JSON.parse 一解析就已经丢精度了。

这个问题从 JSON 诞生起就存在,江湖上流传着各种 workaround:把 ID 转成字符串、引入 json-bigint 库、写 replacer 函数手工拼接。今天,ES2026 用两个标准 API 把这件事彻底原生化了,不需要任何库,也不需要手写字符串拼接。

问题到底出在哪

JavaScript 的 Number 类型是 IEEE 754 双精度浮点,整数精度安全范围是 -2⁵³+1 到 2⁵³-1,也就是 -9007199254740991 到 9007199254740991。超过这个范围的整数,双精度浮点无法精确表示,解析时就已经发生了截断。

const json = '{"orderId": 775692479844696084}';
const parsed = JSON.parse(json);
console.log(parsed.orderId);
// 775692479844696000
// 末尾的 84 被截掉了

这和后端语言无关。后端发来的 JSON 字符串本身是完整的,问题出在 JavaScript 引擎解析 JSON 时,把数字字面量转成了有精度损失的双精度浮点。JSON.stringify 也有类似问题——当你试图把一个大整数序列化回 JSON 时,JavaScript 同样会先把它转成双精度浮点再输出:

const obj = { orderId: 775692479844696084n };
JSON.stringify(obj);
// 抛出 TypeError: BigInt value can't be serialized in JSON

ES2026 的两个解法

ES2026 引入了两个配套 API,解决解析和序列化两端的精度问题。

解析端:context.source

JSON.parse 的 reviver 参数现在接受第三个参数 context,其中 context.source 包含解析时原始 JSON 文本中该值的字面量。这个字面量是字符串形式的,不会受到 JavaScript Number 精度限制的影响。

const json = '{"orderId": 775692479844696084}';
const parsed = JSON.parse(json, (key, value, context) => {
  if (typeof value === 'number' && context?.source) {
    // 如果原始字面量是正负整数,尝试还原 BigInt
    if (/^-?d+$/.test(context.source)) {
      return BigInt(context.source);
    }
  }
  return value;
});
console.log(parsed.orderId);
// 775692479844696084n ✅ 精度完整保留

context.source 的作用域仅限于原始值(即叶子节点),对象和数组本身不提供 source 信息。在遍历顺序上,reviver 遵循从子节点到父节点的顺序(先访问属性,再访问父对象),最后才访问根对象。

序列化端:JSON.rawJSON()

解决了「解析丢精度」的问题,还有一个相反的问题:「序列化时,BigInt 无法直接转 JSON」。传统的做法是用字符串替代,但这会导致类型不统一——同一个字段有时是字符串,有时是数字,接口契约难以维护。

JSON.rawJSON() 创建了一个特殊的「裸 JSON」对象,JSON.stringify 在输出时会把这个对象的内容原样写入,不经过 JavaScript 的类型转换:

const obj = { orderId: 775692479844696084n };
const json = JSON.stringify(obj, (key, value) => {
  if (typeof value === 'bigint') {
    return JSON.rawJSON(value.toString());
  }
  return value;
});
console.log(json);
// {"orderId":775692479844696084} ✅ BigInt 直接序列化为 JSON 数字

JSON.rawJSON() 有几个重要约束:

  • 输入必须是合法的 JSON 原始值(数字、字符串、true、false、null),不能是对象或数组
  • 如果输入不是合法 JSON,直接抛出 SyntaxError
  • 返回值是一个冻结的、prototype 为 null 的特殊对象,JSON.isRawJSON() 可以判断一个对象是否由 JSON.rawJSON() 创建
JSON.isRawJSON(JSON.rawJSON("123")); // true
JSON.isRawJSON({ rawJSON: "123" });  // false

完整的无损 round-trip 方案

把两个 API 结合在一起,就实现了真正的无损 JSON 传输——解析时自动把大整数还原为 BigInt,序列化时再原样写回去,不需要手动转换类型:

// 解析:大整数不丢精度
const parseLossless = (text) => JSON.parse(text, (key, value, ctx) => {
  if (typeof value === 'number' && ctx?.source) {
    if (/^-?d+$/.test(ctx.source)) {
      return BigInt(ctx.source);
    }
  }
  return value;
});

// 序列化:BigInt 直接写回 JSON 数字
const stringifyLossless = (obj) => JSON.stringify(obj, (key, value) => {
  if (typeof value === 'bigint') {
    return JSON.rawJSON(value.toString());
  }
  return value;
});

// 完整 round-trip
const original = { orderId: 775692479844696084n, amount: 42.50 };
const json = stringifyLossless(original);
// '{"orderId":775692479844696084,"amount":42.50}'
const restored = parseLossless(json);
// { orderId: 775692479844696084n, amount: 42.5 }
console.log(original.orderId === restored.orderId); // true ✅

浏览器支持与降级策略

MDN 将 JSON.rawJSON() 标注为 Baseline 2025(2025 年 3 月起主流浏览器全面支持):Chrome 114+、Firefox 135+、Safari 17.4+。JSON.parse reviver 的 context 参数同样是这些版本引入的。

对于需要兼容旧版浏览器的场景,建议做特性检测:

const supportsRawJSON = typeof JSON.rawJSON === 'function' && typeof JSON.isRawJSON === 'function';
const supportsParseSource = (() => {
  let seen = false;
  JSON.parse('{"n":1}', (k, v, c) => { if (c?.source === '1') seen = true; return v; });
  return seen;
})();

if (!supportsRawJSON || !supportsParseSource) {
  // 降级方案:把大整数 ID 字段声明为 string 类型
  // 或引入 json-bigint 库(~5KB)
  console.warn('当前环境不支持无损 JSON,大整数 ID 请用字符串类型');
}

和社区方案的对比

在此之前,精度无损的 JSON 处理有几种常见方案:

方案 原理 体积 精度 类型安全
后端发字符串 服务器直接发 "775692479844696084" 0 ✅ 需接口约定
json-bigint 库 自定义解析器返回 BigInt ~5KB ✅ 需替换 JSON.parse
lossless-json 库 LosslessNumber 保留原始字符串 ~3KB ✅ 需替换 JSON.parse
ES2026 原生 API 两个标准方法,无需依赖 0 ✅ 标准 API

原生 API 的优势是零依赖、标准可迁移、不需要替换全局 JSON 对象,只需要在特定场景下按需使用 replacer/reviver 即可。

下一步

  1. 检查你的接口契约:如果你有接收 64 位整数 ID 的接口(订单、用户、交易 ID),优先推动后端把这类字段改为 string 类型,从源头规避精度问题
  2. 如果接口不可控:在前端用上面的 parseLossless / stringifyLossless 工具函数处理包含大整数的 JSON 数据
  3. TypeScript 增强:用 branded type 防止把 ID 当成普通数字做算术运算:
    type OrderId = string & { readonly __brand: 'OrderId' };

ES2026 把 JSON 精度问题从「需要靠约定或库来解决」,变成了「平台原生能力」。两个 API 一行代码都不需要装,大整数精度丢了几十年的这个问题,今天终于从根上原生化了。

评论区

0 条评论

登录后可评论。

阿柯·前端架构 15 阅读