浮点数精度问题
解释 JavaScript 浮点数为什么会出现精度误差,以及常见的处理方式。
#type / concept
#status / growing
#tech / dev / frontend
浮点数精度问题
一句话定义
JavaScript 使用 IEEE 754 双精度浮点数存储所有 Number 类型,部分十进制小数无法精确表示为二进制小数,导致运算出现精度误差。
核心机制
IEEE 754 双精度结构
一个 64 位浮点数由三部分组成:
- 1 位符号位(sign)
- 11 位指数位(exponent)
- 52 位尾数位(mantissa)
| S | EEEEEEEEEEE | MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM |
1 11 52
为什么 0.1 + 0.2 !== 0.3
十进制的 0.1 转成二进制是无限循环小数:
0.0001100110011001100110011...(1100 循环)
双精度只能存 52 位尾数,截断后产生微小误差。0.1 和 0.2 各自有微小偏差,相加后偏差累积:
0.1 + 0.2
// 0.30000000000000004
0.1 + 0.2 === 0.3
// false
toFixed / toPrecision 的边界
// toFixed 返回字符串,有四舍五入
(1.005).toFixed(2) // "1.00" 而不是 "1.01"
// 因为 1.005 的实际存储值略小于 1.005
// toPrecision 指定有效数字位数
(0.1 + 0.2).toPrecision(1) // "0.3"
(0.1 + 0.2).toPrecision(17) // "0.30000000000000004"
toFixed 和 toPrecision 都不能从根本上解决精度问题,只是在显示层面做四舍五入。
常见处理方式
1. 放大为整数运算
// 金额计算:放大到分
const a = 0.1 * 100 // 10
const b = 0.2 * 100 // 20
(a + b) / 100 // 0.3
2. Number.EPSILON 做误差判断
function equal(a, b) {
return Math.abs(a - b) < Number.EPSILON
}
equal(0.1 + 0.2, 0.3) // true
注意:Number.EPSILON 是 2^-52,约 2.2e-16。对于放大后的整数相减,EPSILON 可能过小。
3. toFixed 取指定位数后转回数字
parseFloat((0.1 + 0.2).toFixed(10)) // 0.3
这是精度够用场景下的快速方案,但本质上是截断而非精确计算。
4. BigInt 处理整数大数
const a = 9007199254740993n // 超过 Number.MAX_SAFE_INTEGER
const b = 1n
a + b // 9007199254740994n
BigInt 只能处理整数,不能直接用于小数运算。金额场景可以全程用”分”的 BigInt 表示。
5. 专用库
decimal.js/big.js/bignumber.js:任意精度小数运算- 金融、科学计算等对精度要求严格的场景推荐使用
边界与常见误解
Number.EPSILON不是”最小精度差”,而是 1 与大于 1 的最小浮点数之差。它只适合判断接近 1 量级的数是否”足够相等”。toFixed返回字符串,不返回数字。parseFloat转回来时可能再次引入误差。Math.round也不能解决浮点问题:Math.round(1.005 * 100) / 100得到 1 而不是 1.01。- BigInt 不能与
Number混合运算,也不能表示小数。 0.1 + 0.2 === 0.3在几乎所有语言中都是false,这不是 JS 的 bug,而是 IEEE 754 的通用行为。