数字精度陷阱与 BigInt 大数处理
0.1 + 0.2 为什么不等于 0.3、Number 的安全整数边界在哪、BigInt 怎么用、大数在前后端 JSON 传输里是怎么悄悄丢精度的——这套精度题几乎每轮面试都会撞上。
一句话概括
数字精度面试题,说白了就一句话:JS 里的数全是用 IEEE 754 双精度浮点存的,整数超出 2^53-1 就会开始”串号”,小数很多从根上就存不精确。所以你会遇到两类坑:小数算不准(0.1+0.2 !== 0.3)和大整数会变形(雪花 ID、订单号、金额)。小数用容差比较或转整数/Decimal,大整数用 BigInt,跨端传输用字符串——这就是全部答案。
核心知识点
1. 为什么 0.1 + 0.2 !== 0.3
JS 的 Number 只有一种,底层是 IEEE 754 双精度:64 位里 1 位符号 + 11 位指数 + 52 位尾数。问题在于,像 0.1 这种十进制小数转成二进制是 无限循环 的(0.0001100110011…),而尾数只有 52 位,只能截断舍入成一个”近似值”。所以 0.1 和 0.2 从被存进内存那一刻起就已经不是精确的 0.1 和 0.2 了,两个近似值相加再舍入,得到 0.30000000000000004。
记住:计算机没算错,它严格地算了两个”已经被舍入过的近似值”。所有用 IEEE 754 的语言(Java、Python、Go…)都有这个现象。
1
2
3
4
5
6
7
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // false
// ❌ 小数别用 === 比
// ✅ 用"足够接近"来比(容差)
const eq = (a, b, eps = Number.EPSILON) => Math.abs(a - b) < eps;
eq(0.1 + 0.2, 0.3); // true(差值约 5.5e-17 < 2.22e-16)
Number.EPSILON 就是 2^-52,是 JS 能区分的最小精度单位,正经做浮点比较就用它兜底。
2. 安全整数边界:2^53 - 1 之后就不可信
另一个大坑是整数。JS 能精确表示的整数范围是 -(2^53-1) 到 2^53-1,即 Number.MAX_SAFE_INTEGER = 9007199254740991。超过这个值,相邻整数在二进制里已经无法区分,会被舍入成同一个数。
1
2
3
4
5
6
7
Number.MAX_SAFE_INTEGER; // 9007199254740991
Number.MAX_SAFE_INTEGER + 1; // 9007199254740992 ← 还"对"
Number.MAX_SAFE_INTEGER + 2; // 9007199254740992 ← 错了!和上一个一样
Number.isSafeInteger(9007199254740992); // false
// 经典翻车:时间戳乘 1000 后再运算、超大 ID
BigInt(Number.MAX_SAFE_INTEGER) + 2n === 9007199254740993n; // true(用 BigInt 才准)
Number.isSafeInteger() 可以提前判断一个数是否落在安全范围内,校验接口返回的大整数时很有用。
3. 金额和小数计算的正确姿势
面试常问”金额怎么存”。结论:展示可以用浮点,账本计算绝不能用普通浮点。
1
2
3
4
5
6
7
8
9
// ❌ 直接拿浮点算钱
0.1 + 0.2; // 0.30000000000000004(分账就错了)
// ✅ 姿势一:转成最小整数单位(元→分)
const total = 10 * 100 + 20 * 100; // 以"分"为单位全程整数运算
(total / 100).toFixed(2); // "0.30" 只在展示时再除
// ✅ 姿势二:用 Decimal.js / big.js 做十进制高精度运算
// new Decimal("0.1").plus("0.2").toString() // "0.3"(务必用字符串构造!)
重点:Decimal/BigDecimal 一定要用字符串构造。如果你写
new Decimal(0.1),0.1 在传进去之前就已经先变成了不精确的 double,根上就救不回来了。
4. BigInt:超出安全范围的整数救星(ES2020)
ES2020 引入了 bigint 类型,专门表示任意精度的大整数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 两种创建方式
const a = 9007199254740993n; // 字面量加 n
const b = BigInt("9007199254740993"); // 构造函数(推荐用字符串)
BigInt(9007199254740993); // 9007199254740992n ← 传 number 已失真,慎用!
typeof a; // "bigint"
a === 9007199254740993; // false(和 number 不是同一类型,严格相等直接 false)
10n === 10; // false(同理:类型不同)
10n == 10; // true(宽松相等会转数字,但不建议依赖,类型语义不清)
// ✅ 大整数运算精确
a + 1n; // 9007199254740994n
// ❌ 不能和 Number 混算,会直接 TypeError
a + 1; // TypeError: Cannot mix BigInt and other types
几个易错点,面试常挖:
- BigInt 和 Number 不要混运算(除了比较运算符
>、<、==这类返回布尔的,它们不会丢精度,可以混用)。 - 一元
+不支持(+1n报错),因为会和把字符串转 number 的逻辑冲突;但一元-可以表示负数(-42n)。 - 无符号右移
>>>对 BigInt 无效,因为 BigInt 始终是有符号的。 JSON.stringify不支持 BigInt,会直接抛TypeError:
1
JSON.stringify({ id: 1n }); // ❌ TypeError: Do not know how to serialize a BigInt
5. 大数在前后端传输里的隐形坑
这是最容易被忽略的生产级坑:后端 bigint 字段(如雪花 ID、订单号)一旦经 JSON 传给前端,会自动变成 Number,超 2^53 直接丢精度。根本原因有两点:
- JS 的
Number上限就是 2^53-1(第 2 节); - JSON 标准本身没有 BigInt 类型,只有
number和string。
1
2
3
4
5
6
7
8
// 后端返回 9223372036854775807(MySQL bigint),前端收到后:
console.log(9223372036854775807); // 9223372036854776000 ← 末尾直接变形!
// ✅ 解法:后端把 bigint 转成字符串再返回
fetch('/user').then(r => r.json()).then(d => {
const id = BigInt(d.id); // 字符串 → BigInt,精确
id + 1n;
});
这条几乎是”接口对接真实踩坑”题,答出来比背浮点原理加分。前端这边拿到的如果是字符串,
BigInt()还原即可;需要运算就用 BigInt 或大数库,千万别先Number()再算。
其实你每天都在用
- 购物车金额:加个优惠券、算个折扣,全是用”分”做整数运算再
toFixed(2)展示,不然对账对不上。 - 接口返回的订单号/用户 ID:后端是
bigint,你看到前端id末尾几位变成了 0 或乱码,九成是没转字符串。 0.1 + 0.2的打印:控制台随手一敲就能复现,是面试官最爱拿来开场的热身题。Number.isSafeInteger校验:拿到后端大整数先判一下是否安全,不安全就走字符串/BigInt 逻辑。performance.now()、时间戳运算:毫秒/微秒级时间差经常涉及小数,UI 动画里基本忽略,但算耗时统计要注意。
常见误解(FAQ)
❌ 误区1:”0.1+0.2 不等于 0.3 是 JS 的 bug。” 错。这是 IEEE 754 浮点数的通病,Java/Python/Go 全都一样。JS 只是把最真实的近似值打印出来了。
❌ 误区2:”BigInt 比 Number 更好,以后全用 BigInt。” 错。BigInt 运算比 Number 慢,而且不能和 Number 混用、不能用 Math 上的方法(如 Math.max(1n, 2n) 报错)。只在”确实需要超安全范围的整数”时才用。
❌ 误区3:”Decimal.js 用数字构造也行。” 错。必须用字符串 new Decimal("0.1")。用 new Decimal(0.1) 的话,0.1 在传入前就已经被 double 污染了。
❌ 误区4:”'9007199254740993' - 0 能拿到精确大整数。” 错。- 0 会把它转成 Number,超过安全范围照样失真。要精确就 BigInt('9007199254740993')。
❌ 误区5:”JSON 能原样传 BigInt。” 错。JSON.stringify 遇到 BigInt 直接抛 TypeError,根本序列化不出来。后端必须转字符串。
一句话总结
小数存不精确就用容差(Number.EPSILON)或转整数/Decimal,大整数超 2^53-1 就用 BigInt,跨端大数一律走字符串——别让浮点的”近似”污染了你的账本和 ID。