问题提出

1
2
10.23 + 0.2345;
0.1 + 0.2;

10.23 + 0.2345 实际测试结果为10.464500000000001,而不是10.4645
0.1 +0.2 实际测试结果为0.30000000000000004,而不是0.3

原因

浮点数在 2 进制中存储的时候,无法精确存储

js 在数据存储中采用 64 位双精度浮点数形式储存

整数转为 2 进制

用 8 位 2 进制表达 10 进制下的数字 8

1
2
3
4
8/2 = 4 => 0
4/2 = 2 => 0
2/2 = 1 => 0
1/2 = 0 => 1

商为 0 终止计算,然后倒排为 1000,因为是 8 位,所以在上 4 位补 0
表达即为:00001000

浮点数转换为 2 进制

例如 0.82 的二进制表达为:

1
2
3
4
5
6
7
8
0.82 _ 2 = 1.64 => 1
0.64 _ 2 = 1.28 => 1
0.28 _ 2 = 0.56 => 0
0.56 _ 2 = 1.12 => 1
0.12 _ 2 = 0.24 => 0
0.24 _ 2 = 0.48 => 0
0.48 _ 2 = 0.96 => 0
0.96 _ 2 = 1.92 => 1

表达即为:0.11010001…

举例:10 进制下 0.1 表达为二进制下为:0.000110011…

浮点数在 64 位双精度下的存储

符号位 S:第 1 位是正负数符号位(sign),0 代表正数,1 代表负数

指数位 E:中间的 11 位存储指数(exponent),用来表示次方数

尾数位 M:最后的 52 位是尾数(mantissa),超出的部分自动进一舍零

公式表示:

以上的公式遵循科学计数法的规范,在十进制是为 0<M<10,到二进行就是 0<M<2。也就是说整数部分只能是 1,所以可以被舍去,只保留后面的小数部分。如 4.5 转换成二进制就是 100.1,科学计数法表示是 1.001*2^2,舍去 1 后 M = 001。E 是一个无符号整数,因为长度是 11 位,取值范围是 0~2047。但是科学计数法中的指数是可以为负数的,所以再减去一个中间数 1023,[0,1022]表示为负,[1024,2047] 表示为正。如 4.5 的指数 E = 1025,尾数 M 为 001

0.1 转成二进制表示为 0.0001100110011001100(1100 循环),1.100110011001100x2^-4,所以 E=-4+1023=1019;M 舍去首位的 1,得到 100110011…

0.1 +0.2

1
2
3
4
5
// 0.1 和 0.2 都转化成二进制后再进行运算
0.0001100110011001100110011001100110011001100110011001101 +
0.001100110011001100110011001100110011001100110011001101 = 0.0100110011001100110011001100110011001100110011001100111;

// 转成十进制正好是 0.30000000000000004

生产环境中的解决办法

https://github.com/dt-fe/number-precision