因为 JS 采用 IEEE 754 双精度版本(64位),并且只要采用 IEEE 754 的语言都有该问题。
我们都知道计算机表示十进制是采用二进制表示的。
1. 基础
双精度浮点数的结构如下:
1位符号 | 11位指数 | 52位尾数
符号位:0表示正数,1表示负数
指数:采用偏移量为1023的表示法(即实际指数 + 1023)
尾数:标准化后的有效数去掉开头的1,并截取52位。
2. 将十进制小数0.1转换为二进制:
十进制到二进制转换:通过逐步乘以2,取整数部分作为二进制小数部分的下一位,取小数部分继续乘以2。重复此过程直到小数部分为0或达到所需的精度。
0.1 * 2 = 0.2 -> 整数部分为0,小数部分为0.2
0.2 * 2 = 0.4 -> 整数部分为0,小数部分为0.4
0.4 * 2 = 0.8 -> 整数部分为0,小数部分为0.8
0.8 * 2 = 1.6 -> 整数部分为1,小数部分为0.6
0.6 * 2 = 1.2 -> 整数部分为1,小数部分为0.2
0.2 * 2 = 0.4 -> 整数部分为0,小数部分为0.4
......
这个过程会继续下去,结果是一个无限循环小数:0.00011001100110011001100110011...
。
小数算二进制和整数不同。乘法计算时,只计算小数位,整数位用作每一位的二进制,并且得到的第一位为最高位。所以我们得出 0.1 = 1.1001100110011001100110011001100110011001100110011001×2^−4
3. 将十进制小数0.2转换为二进制:
0.2 * 2 = 0.4 -> 整数部分为0,小数部分为0.4
0.4 * 2 = 0.8 -> 整数部分为0,小数部分为0.8
0.8 * 2 = 1.6 -> 整数部分为1,小数部分为0.6
0.6 * 2 = 1.2 -> 整数部分为1,小数部分为0.2
0.2 * 2 = 0.4 -> 整数部分为0,小数部分为0.4
0.4 * 2 = 0.8 -> 整数部分为0,小数部分为0.8
那么 0.2 的演算也基本如上所示,只需要去掉第一步乘法,所以得出 0.2 = 1.1001100110011001100110011001100110011001100110011001×2^−3
4. 具体计算
- 对齐指数
将0.1的尾数右移一位对齐0.2的指数:
0.1 尾数(右移一位): 0.110011001100110011001100110011001100110011001100110011001100
0.2 尾数: 1.100110011001100110011001100110011001100110011001100110011001
- 尾数相加
将对齐后的尾数相加:
0.110011001100110011001100110011001100110011001100110011001100
+ 1.100110011001100110011001100110011001100110011001100110011001
---------------------------------------------------------------
10.011001100110011001100110011001100110011001100110011001100101
由于结果超过了1,我们需要规范化:
1.001100110011001100110011001100110011001100110011001100110010 × 2^{-2}
尾数部分为:1.001100110011001100110011001100110011001100110011001100110010
- 将尾数部分转换成十进制:
1+ 0×2^−1
+ 0×2^−2
+ 1×2^−3
+ 1×2^−4
+ 0×2^−5
+ 0×2^−6
+ 1×2^−7
+ 1×2^−8
+ …
=
1 + 1/8
+ 1/16
+ 1/128
+ 1/256
+ 1/2048
+ 1/4096
+ 1/32768
+ 1/65536
+ 1/524288
+ …
=
1 + 0.125
+ 0.0625
+ 0.0078125
+ 0.00390625
+ 0.00048828125
+ 0.000244140625
+ 0.000030517578125
+ 0.0000152587890625
+ 0.0000019073486328125
+ …
≈ 1.1999999999999999555910790149937383830547332763671875
最终结果
1.1999999999999999555910790149937383830547332763671875 x 2^{-2}
= 1.1999999999999999555910790149937383830547332763671875 / 4
≈ 0.3000000000000000444089209850062616169452667236328125
5. 解决办法
- 固定小数位数比较
将浮点数运算结果保留一定的小数位数,然后进行比较:
function isEqual(a, b, epsilon = Number.EPSILON) {
return Math.abs(a - b) < epsilon;
}
const result = 0.1 + 0.2;
console.log(isEqual(result, 0.3)); // 输出: true
- 转换成整数进行计算
将浮点数转换为整数进行计算,确保运算的精度:
function add(a, b) {
const factor = Math.pow(10, 10); // 选择足够大的因子
return (Math.round(a * factor) + Math.round(b * factor)) / factor;
}
const result = add(0.1, 0.2);
console.log(result === 0.3); // 输出: true
- 指定小数位数进行计算
parseFloat((0.1 + 0.2).toFixed(10))
- 使用Big.js第三方库
const a = new Big(0.1);
const b = new Big(0.2);
const sum = a.plus(b); // 加法