JS浮点数精度问题和解决办法
# 一、现象
在 js 计算时,偶尔会出现精度问题,一些常见的例子如下:
// 加法 =====================
0.1 + 0.2 = 0.30000000000000004
0.7 + 0.1 = 0.7999999999999999
0.2 + 0.4 = 0.6000000000000001
// 减法 =====================
1.5 - 1.2 = 0.30000000000000004
0.3 - 0.2 = 0.09999999999999998
// 乘法 =====================
19.9 * 100 = 1989.9999999999998
0.8 * 3 = 2.4000000000000004
35.41 * 100 = 3540.9999999999995
// 除法 =====================
0.3 / 0.1 = 2.9999999999999996
0.69 / 10 = 0.06899999999999999
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 二、js中浮点数的存储
JavaScript中所有数字包括整数和小数都只有一种类型 — Number。使用64位固定长度来表示,也就是标准的 double 双精度浮点数
双精度浮点数在内存中占8个字节、有效数字16位、表示范围:-1.79E+308 ~ +1.79E+308
图解:
位用来表示符号位 1表示正数,0表示负数
1位用来表示指数
2位表示小数部分
# 三、js小数计算过程
我们看一下计算机是怎么算出 0.1 +0.2===0.30000000000000004
- 小数转二进制 0.0001100110011001100110011001100110011001100110011001101
- 转科学记数法 1.100110011001100110011001100110011001100110011001101 * 2^(-4)
- 存储为[64位]形式 (符号位+(指数位+指数偏移量)+小数部分) 0011111110111001100110011001100110011001100110011001100110011010
同理,0.2计算后得到
0011111111001001100110011001100110011001100110011001100110011010
可以看出来在转换为二进制时
0.1 >>> 0.0001 1001 1001 1001...(1001无限循环)
0.2 >>> 0.0011 0011 0011 0011...(0011无限循环)
就像一些无理数不能无限表示,如 圆周率 3.1415926...,1.3333... 等
在转换为二进制的科学记数法的形式时只保留64位有效的数字,此时只能模仿十进制进行四舍五入了
但是二进制只有 0 和 1 两个,于是变为 0 舍 1 入,在这一步出现了错误。
那么一步错步步错,那么在计算机存储小数时也就理所应当的出现了误差。这即是计算机中部分浮点数运算时出现误差,这就是丢失精度的根本原因
0.00011001100110011001100110011001100110011001100110011010
+0.00110011001100110011001100110011001100110011001100110100
=0.01001100110011001100110011001100110011001100110011001110
0.1+0.2 >> 0.0100 1100 1100 1100...(1100无限循环)
则0.1 + 0.2的结果的二进制数科学记数法表示为为
1.001100110011001100110011001100110011001100110011010 * 2^(-2),
省略尾数最后的0,即
1.00110011001100110011001100110011001100110011001101 * 2^(-2)
因此(0.1+0.2)实际存储时的形式是
0011111111010011001100110011001100110011001100110011001100110100
因计算机存储位数的限制而截断的二进制数字,再转换为十进制,就成了
0.30000000000000004
# 四、js中的小数都不准确吗?
存储二进制时小数点的偏移量最大为52位,最多可表示的十进制为9007199254740992,对应科学计数尾数是 9.007199254740992,这也是 JS 最多能表示的精度。
它的长度是 16,所以 可以使用 toPrecision(16) 来做精度运算,
js自动做了这一部分处理,超过的精度会自动做凑整处理。
于是就有:
0.10000000000000000555.toPrecision(16) //0.1000000000000000 去掉末尾的零后正好为0.1
2
但你看到的 0.1
实际上并不是 0.1
。可用更高的精度试试:
0.1.toPrecision(21) // 0.100000000000000005551
# 五、总结
计算机存储双精度浮点数需要先把十进制数转换为二进制的科学记数法的形式,然后计算机以自己的规则 {符号位+(指数位+指数偏移量的二进制)+小数部分} 存储二进制的科学记数法,因为存储时有位数限制(64位),并且某些十进制的浮点数在转换为二进制数时会出现无限循环,会造成二进制的舍入操作(0舍1入),当再转换为十进制时就造成了计算误差。