为什么 0.1 + 0.2 不等于 0.3
一、现象
1 | 0.1 + 0.2; // 0.30000000000000004 |
这不是 JavaScript 的 Bug,而是所有使用 IEEE 754 标准的编程语言(Python、Java、Ruby、C++ 等)都会遇到的问题。
1 | # Python 同样存在 |
二、计算机如何表示数字
整数是精确的
计算机用二进制表示整数,每一位是 0 或 1:
1 | 十进制 5 → 二进制 101 → 1×2² + 0×2¹ + 1×2⁰ |
只要位数足够,任何整数都可以精确地用二进制表示。
小数需要”翻译”
小数的二进制表示是 2 的负幂次:
1 | 十进制 0.5 → 二进制 0.1 → 1 × 2⁻¹ |
问题是:不是所有十进制小数都能用有限位二进制精确表示。
三、核心原因:二进制无法精确表示 0.1
计算 0.1 的二进制
用”乘 2 取整”法计算 0.1 的二进制:
1 | 0.1 × 2 = 0.2 → 取整数部分 0 |
可以看到,这个过程会无限循环。0.1 的二进制是:
1 | 0.0001100110011001100110011001100110011... (0011 无限循环) |
就像十进制无法精确表示 1/3(0.3333…)一样,二进制也无法精确表示 0.1。
0.2 同样如此:
1 | 0.2 的二进制:0.0011001100110011001100110011... (0011 无限循环) |
计算机只能截断
计算机的存储空间有限,不可能存储无限循环的二进制。JavaScript 使用 64 位双精度浮点数(IEEE 754 标准),只能保留 53 位有效数字,多余的位数会被截断(舍入)。
因此,计算机中实际存储的 0.1 并不是真正的 0.1,而是最接近 0.1 的近似值:
1 | 0.1 ≈ 0.1000000000000000055511151231257827021181583404541015625 |
两个近似值相加,结果略大于 0.3:
1 | 0.1 + 0.2 ≈ 0.3000000000000000444089209850062616169452667236328125 |
这就是 0.30000000000000004 的由来。
四、IEEE 754 双精度浮点数
JavaScript 的数字都遵循 IEEE 754 双精度浮点数标准(64 位):
1 | ┌─ 1位 ─┬───── 11位 ────┬───────────── 52位 ─────────────┐ |
- 符号位(1 位):0 正数,1 负数
- 指数位(11 位):决定数值范围
- 尾数位(52 位):决定数值精度(实际有效 53 位,因为第一位隐含的 1 不存储)
精度限制
尾数位有 52 位,加上隐含的 1 共 53 位有效数字。这意味着:
整数安全范围:-2⁵³ + 1 到 2⁵³ - 1(即 -9007199254740991 到 9007199254740991)
1 | Number.MAX_SAFE_INTEGER; // 9007199254740991 |
浮点精度:约 15-17 位有效十进制数字。超过这个范围的数字会被截断:
1 | 0.1234567890123456789; // 0.12345678901234568(丢失了最后几位) |
五、更多案例
1 | 0.1 + 0.2; // 0.30000000000000004 |
规律:只有分母是 2 的幂(1、2、4、8、16…)的小数才能精确表示。分母包含其他因子(3、5、7、10…)的小数都可能不精确。
六、解决方案
1. 使用 toFixed 或 toPrecision(显示用)
1 | (0.1 + 0.2).toFixed(1); // "0.3"(字符串) |
注意 toFixed 在不同浏览器中有不同的舍入行为,且返回字符串。
2. 误差容差(epsiolon)
判断浮点数相等时,允许一个很小的误差范围:
1 | function approxEqual(a, b, eps = 1e-10) { |
ES6 提供了 Number.EPSILON(表示 1 与大于 1 的最小浮点数之差):
1 | function epsilonEqual(a, b) { |
3. 转为整数计算
对于货币或固定精度的数值,先放大为整数再计算:
1 | // 价格计算:保留两位小数 |
4. 使用第三方库
对于高精度需求,使用专门的高精度计算库:
1 | // decimal.js |
5. Number.EPSILON 的局限性
Number.EPSILON 适用于数值接近 1 的场景,但不适用于大数值或极小数:
1 | // 大数值场景 |
七、总结
1 | 0.1 → 二进制循环小数 → 存储时截断 → 实际是近似值 |
根本原因:
- IEEE 754 双精度浮点数的二进制表示无法精确存储某些十进制小数
- JavaScript 的数字全部是双精度浮点数(没有整数类型)
解决方案:
- 显示场景用
toFixed - 比较场景用误差容差(
Number.EPSILON) - 高精度场景用整数放大或第三方库