为什么 0.1 + 0.2 不等于 0.3

一、现象

1
2
0.1 + 0.2;            // 0.30000000000000004
0.1 + 0.2 === 0.3; // false

这不是 JavaScript 的 Bug,而是所有使用 IEEE 754 标准的编程语言(Python、Java、Ruby、C++ 等)都会遇到的问题。

1
2
# Python 同样存在
0.1 + 0.2 # 0.30000000000000004

二、计算机如何表示数字

整数是精确的

计算机用二进制表示整数,每一位是 0 或 1:

1
2
十进制 5  →  二进制 101    →  1×2² + 0×2¹ + 1×2⁰
十进制 13 → 二进制 1101 → 1×2³ + 1×2² + 0×2¹ + 1×2⁰

只要位数足够,任何整数都可以精确地用二进制表示。

小数需要”翻译”

小数的二进制表示是 2 的负幂次:

1
2
3
十进制 0.5   →  二进制 0.1     →  1 × 2⁻¹
十进制 0.25 → 二进制 0.01 → 1 × 2⁻²
十进制 0.75 → 二进制 0.11 → 1×2⁻¹ + 1×2⁻²

问题是:不是所有十进制小数都能用有限位二进制精确表示

三、核心原因:二进制无法精确表示 0.1

计算 0.1 的二进制

用”乘 2 取整”法计算 0.1 的二进制:

1
2
3
4
5
6
7
8
9
0.1 × 2 = 0.2   →  取整数部分 0
0.2 × 2 = 0.4 → 取整数部分 0
0.4 × 2 = 0.8 → 取整数部分 0
0.8 × 2 = 1.6 → 取整数部分 1 → 剩余 0.6
0.6 × 2 = 1.2 → 取整数部分 1 → 剩余 0.2
0.2 × 2 = 0.4 → 取整数部分 0
0.4 × 2 = 0.8 → 取整数部分 0
0.8 × 2 = 1.6 → 取整数部分 1 → 剩余 0.6
...

可以看到,这个过程会无限循环。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
2
0.1 ≈ 0.1000000000000000055511151231257827021181583404541015625
0.2 ≈ 0.200000000000000011102230246251565404236316680908203125

两个近似值相加,结果略大于 0.3:

1
0.1 + 0.2 ≈ 0.3000000000000000444089209850062616169452667236328125

这就是 0.30000000000000004 的由来。

四、IEEE 754 双精度浮点数

JavaScript 的数字都遵循 IEEE 754 双精度浮点数标准(64 位):

1
2
3
4
┌─ 1位 ─┬───── 11位 ────┬───────────── 52位 ─────────────┐
│ 符号位 │ 指数位 │ 尾数位 │
│ (s) │ (e) │ (f) │
└───────┴──────────────┴─────────────────────────────────┘
  • 符号位(1 位):0 正数,1 负数
  • 指数位(11 位):决定数值范围
  • 尾数位(52 位):决定数值精度(实际有效 53 位,因为第一位隐含的 1 不存储)

精度限制

尾数位有 52 位,加上隐含的 1 共 53 位有效数字。这意味着:

整数安全范围-2⁵³ + 12⁵³ - 1(即 -9007199254740991 到 9007199254740991)

1
2
3
4
5
Number.MAX_SAFE_INTEGER;   // 9007199254740991
Number.MIN_SAFE_INTEGER; // -9007199254740991

// 超出安全范围会丢失精度
9007199254740993; // 9007199254740992

浮点精度:约 15-17 位有效十进制数字。超过这个范围的数字会被截断:

1
0.1234567890123456789;      // 0.12345678901234568(丢失了最后几位)

五、更多案例

1
2
3
4
5
6
7
8
9
10
11
0.1 + 0.2;        // 0.30000000000000004
0.3 - 0.1; // 0.19999999999999998
0.2 * 0.1; // 0.020000000000000004
0.3 / 0.1; // 2.9999999999999996

// 精确的十进制分数也可能出问题
1.0 - 0.9; // 0.09999999999999998

// 不是所有小数都有问题
0.5 + 0.25; // 0.75(精确,因为 0.5=2⁻¹, 0.25=2⁻²)
0.125 + 0.125; // 0.25(精确,因为 0.125=2⁻³)

规律:只有分母是 2 的幂(1、2、4、8、16…)的小数才能精确表示。分母包含其他因子(3、5、7、10…)的小数都可能不精确。

六、解决方案

1. 使用 toFixed 或 toPrecision(显示用)

1
2
(0.1 + 0.2).toFixed(1);          // "0.3"(字符串)
+(0.1 + 0.2).toFixed(1); // 0.3(转回数字)

注意 toFixed 在不同浏览器中有不同的舍入行为,且返回字符串。

2. 误差容差(epsiolon)

判断浮点数相等时,允许一个很小的误差范围:

1
2
3
4
5
function approxEqual(a, b, eps = 1e-10) {
return Math.abs(a - b) < eps;
}

approxEqual(0.1 + 0.2, 0.3); // true

ES6 提供了 Number.EPSILON(表示 1 与大于 1 的最小浮点数之差):

1
2
3
4
5
function epsilonEqual(a, b) {
return Math.abs(a - b) < Number.EPSILON;
}

epsilonEqual(0.1 + 0.2, 0.3); // true

3. 转为整数计算

对于货币或固定精度的数值,先放大为整数再计算:

1
2
3
4
5
6
7
8
9
10
11
// 价格计算:保留两位小数
function addPrice(a, b) {
return (a * 100 + b * 100) / 100;
}

addPrice(0.1, 0.2); // 0.3

// 更安全的取整
function formatPrice(price) {
return Math.round(price * 100) / 100;
}

4. 使用第三方库

对于高精度需求,使用专门的高精度计算库:

1
2
3
4
5
6
7
// decimal.js
import Decimal from 'decimal.js';
new Decimal(0.1).plus(0.2).toNumber(); // 0.3

// bignumber.js
// big.js
// 或 JavaScript 内置的 BigInt(仅适用于整数)

5. Number.EPSILON 的局限性

Number.EPSILON 适用于数值接近 1 的场景,但不适用于大数值或极小数

1
2
3
4
5
6
7
8
9
// 大数值场景
100000000000000.01 + 0.01 === 100000000000000.02; // false 但 epsilonEqual 返回 false

// 因为误差的 scale 不同,需要相对误差而非绝对误差
function relativeEqual(a, b) {
const diff = Math.abs(a - b);
const maxVal = Math.max(Math.abs(a), Math.abs(b), 1);
return diff < Number.EPSILON * maxVal;
}

七、总结

1
2
3
4
5
0.1 → 二进制循环小数 → 存储时截断 → 实际是近似值
+
0.2 → 二进制循环小数 → 存储时截断 → 实际是近似值
=
0.30000000000000004 ≠ 0.3

根本原因:

  1. IEEE 754 双精度浮点数的二进制表示无法精确存储某些十进制小数
  2. JavaScript 的数字全部是双精度浮点数(没有整数类型)

解决方案:

  • 显示场景用 toFixed
  • 比较场景用误差容差(Number.EPSILON
  • 高精度场景用整数放大或第三方库