js 大数精度丢失怎么解决
JS 的 Number 受 IEEE 754 双精度限制,超过 2^53-1 的整数和部分小数会丢精度;大整数用 BigInt 或字符串传输,金额用 decimal.js 或整数分运算,后端 Long 字段序列化为字符串返回。
JS 的 Number 类型是 IEEE 754 双精度浮点数,只能精确表示 2^53-1(即 9007199254740991)以内的整数,超出这个范围或涉及小数运算就会丢精度。解决方案按场景分三类:大整数用 BigInt 或全程字符串传输,金额和交易计算用 decimal.js 或「整数分」运算,接口层面让后端把 Long 型字段序列化成字符串返回。
为什么 JS 会精度丢失?
结论:丢精度的根因是 Number 用 64 位存浮点数——1 位符号 + 11 位指数 + 52 位尾数,加上隐含的首位 1,有效精度只有 53 个二进制位。
超过 53 位的整数会被截断,所以下面两行代码的结果是 true 和 9007199254740992:
9007199254740992 + 1 === 9007199254740992 // true
9007199254740993 // 实际得到 9007199254740992
小数同理。0.1 的二进制是无限循环的 0.00011001100110011...,存进 52 位尾数必须截断,于是:
0.1 + 0.2 // 0.30000000000000004
0.1 + 0.2 === 0.3 // false
(1.005).toFixed(2) // "1.00",不是 "1.01"
怎么快速判断一个数已经不安全了?
结论:用 Number.isSafeInteger(x) 判断整数是否在安全范围内,跨过 Number.MAX_SAFE_INTEGER(9007199254740991)就一定会出问题。
两个直接信号:数字打印出来和原值不一致(末位被改成 0 或 2),以及 0.1 + 0.2 !== 0.3 这类等式不成立。判断浮点相等不要用 ===,用容差比较:
Math.abs(a - b) < Number.EPSILON // EPSILON ≈ 2.22e-16
注意容差比较只适合做「近似相等」判断,金额对账不能用它,必须换成整数或 decimal 方案。
大整数(超过 2^53)怎么处理?
结论:纯计算场景用 BigInt,跨进程传输场景用字符串,两者不要混用。
BigInt 写法是在字面量后加 n,或调 BigInt("9007199254740993"):
const a = 9007199254740993n;
a + 1n // 9007199254740994n
BigInt(Number.MAX_SAFE_INTEGER) + 1n // 9007199254740992n
BigInt 有四个必须知道的限制:不能和 Number 混合运算(1n + 1 直接抛 TypeError);除法向零截断(5n / 2n === 2n,-5n / 2n === -2n);JSON.stringify(1n) 抛 TypeError,需要自定义 replacer 转成字符串;不支持 Math 系列方法和 toFixed。
后端返回 19 位 ID 被改掉末几位怎么修?
结论:让后端把 Long 型字段序列化成字符串,前端拿到后原样当字符串用,全程不 Number()、不 JSON.parse 成数字。
Java 侧给字段加 @JsonSerialize(using = ToStringSerializer.class),或全局配置 Jackson 把 Long/long 输出为 String。前端如果没法改后端,可以在 axios 的 transformResponse 阶段用 json-bigint 接管解析:
import JSONbig from 'json-bigint';
const client = axios.create({
transformResponse: [data => JSONbig({ storeAsString: true }).parse(data)]
});
storeAsString: true 让超出安全范围的整数直接变成字符串,这是最省事的开关。雪花 ID、订单号、流水号、纳秒时间戳都属于这一类,前端只做展示和透传,不做加减。
小数和金额计算怎么避免 0.1+0.2 问题?
结论:金额一律换算成整数最小单位(分)存储和运算,必须用小数时上 decimal.js,且入参传字符串而不是 Number。
方案一,整数分:19.99 元存 1999,展示时 (1999 / 100).toFixed(2),加减乘全在整数域完成,除法则最后一步再做。方案二,decimal.js:
import Decimal from 'decimal.js';
new Decimal('0.1').plus('0.2').toString() // "0.3"
new Decimal('1.005').toFixed(2) // "1.01"
new Decimal('0.1').plus(0.2).toString() // 传 Number 会带入误差,务必用字符串
decimal.js 默认精度 20 位有效数字,可通过 Decimal.set({ precision: 30 }) 调整。同类库还有 big.js(体积小、无精度配置)和 bignumber.js,选哪个都行,关键是别在业务里手写 parseFloat 兜底——parseFloat 同样基于双精度,救不回来。
怎么选:一张按场景对应的对照
- 页面上的 ID、单号、卡号:字符串,禁止转数字
- 大整数运算(哈希、加密、计数):BigInt
- 金额、税率、分摊、对账:整数分 或 decimal.js
- 浮点近似比较:
Number.EPSILON容差 - 接口解析:后端 Long→String,或前端 json-bigint
一句话收束:JS 精度丢失不是「算错了」,而是二进制浮点的表示上限,凡是超过 9007199254740991 的整数和带小数点的金额,都要在数据进入 JS 的那一刻就决定用字符串还是 BigInt 承载,等到 parseFloat 之后再补救已经晚了。相关讨论可以在站内「技术交流 - 前端开发 - JS」版块发帖交流。
转载请注明出处,版权归原作者所有。