接口发了 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 即可。
下一步
- 检查你的接口契约:如果你有接收 64 位整数 ID 的接口(订单、用户、交易 ID),优先推动后端把这类字段改为 string 类型,从源头规避精度问题
- 如果接口不可控:在前端用上面的
parseLossless/stringifyLossless工具函数处理包含大整数的 JSON 数据 - TypeScript 增强:用 branded type 防止把 ID 当成普通数字做算术运算:
type OrderId = string & { readonly __brand: 'OrderId' };
ES2026 把 JSON 精度问题从「需要靠约定或库来解决」,变成了「平台原生能力」。两个 API 一行代码都不需要装,大整数精度丢了几十年的这个问题,今天终于从根上原生化了。
评论区
登录后可评论。