之前在这篇文章中解释了0.1+0.2为什么不等于0.3而等于0.30000000000000004的原因,根源问题还在于存储精度的问题,当时只是简单觉得丢失精度后的行为是理所当然的,并没有丢失精度后是如何处理的,下面就来分析一下:

精度丢失加法

首先,JS,或者说double类型能精确表达的最大的数是Math.pow(2, 53),那么下面表达式的值你肯定也知道了

1
2
Math.pow(2, 53) + 1 === 9007199254740992
Math.pow(2, 53) + 2 === 9007199254740994

那么下面表达式的值呢:

1
Math.pow(2, 53) + 3 ?

可能一开始你会想当然得认为上面的值是:9007199254740994,因为精度不到,所以就把这位丢弃,然而,确切的结果是:

1
9007199254740996

精度丢失算法

显然上面的运算都是在精度丢失的场景下进行的,那么,如何保证精度尽量正确就是必须要做的事,

0舍1进

舍0进1类似4舍5入,0就舍弃,1就向前进一位,举个例子

Math.pow(2, 53) - 1在内存中存储方式为:

1
0|10000110011|1111111111111111111111111111111111111111111111111111

之前我们说到有这么一个公式(注意,这里是二进制运算,那个2的N次方可以理解为十进制的10的N次方):

带入上述计算公式:

1
2
3
符号位 s = 0
指数位 E = 52
整数位 M = 1.1111111111111111111111111111111111111111111111111111

所以最终结果就是M的小数点往右移动52位的结果。

Math.pow(2, 53)在内存中存储方式为:

1
0|10000110100|0000000000000000000000000000000000000000000000000000
1
2
3
符号位 s = 0
指数位 E = 1023 + 52 + 1 = 1077
整数位 M = `1.0000000000000000000000000000000000000000000000000000`

最终结果为M往右移动53位

Math.pow(2, 53) + 1在内存中存储方式为:

1
0|10000110100|0000000000000000000000000000000000000000000000000000

可以看到和Math.pow(2, 53)一致

Math.pow(2, 53) + 2在内存中存储方式为:

1
0|10000110100|0000000000000000000000000000000000000000000000000001

可以看到最后一位多了一个1

1
2
3
符号位 s = 0
指数位 E = 53
整数位 M = 1.0000000000000000000000000000000000000000000000000001

最终结果也就是M往右移动53位,变成了10000000000000000000000000000000000000000000000000001|0

因为右边只有52位,而我们的指数位有53位,那么最后一位就变成0了,也就是竖线后面的那一位,所以这也是精度丢失的根本原因。

Math.pow(2, 53) + 3在内存中存储方式为:

1
0|10000110100|0000000000000000000000000000000000000000000000000010
1
2
3
符号位 s = 0
指数位 E = 53
整数位 M = 1.0000000000000000000000000000000000000000000000000010

最终结果:10000000000000000000000000000000000000000000000000010|0

结论

Math.pow(2, 53)的整数位:0000000000000000000000000000000000000000000000000000|0

后面那个|符号我们可以认为是小数点,来理解0舍1进

  1. [a]当Math.pow(2, 53) + 1时,变成..000|1,1的前值为0,舍弃

  2. [b]当Math.pow(2, 53) + 2时,变成..001|0,舍弃尾部,值精确

  3. [c]当Math.pow(2, 53) + 3时,变成..001|1,1的前值为1,舍0进1,变成,..010|0

  4. [c]当Math.pow(2, 53) + 4时,变成..010|0,舍弃尾部,值精确
  5. [c]当Math.pow(2, 53) + 5时,变成..010|1,1的前值为0,舍弃

  6. [d]当Math.pow(2, 53) + 6时,变成..011|0,舍弃尾部,值精确

  7. [e]当Math.pow(2, 53) + 7时,变成..011|1,1的前值为1,舍0进1,变成, ..100|0

  8. [e]当Math.pow(2, 53) + 8时,变成..100|0,舍弃尾部,值精确
  9. [e]当Math.pow(2, 53) + 9时,变成..100|1,1的前值为0,舍弃

  10. [f]当Math.pow(2, 53) + 10时,变成..101|0,舍弃尾部,值精确

  11. [g]当Math.pow(2, 53) + 11时,变成..101|1,1的前值为1,舍0进1,变成, ..110|0

  12. [g]当Math.pow(2, 53) + 12时,变成..110|0,舍弃尾部,值精确
  13. [g]当Math.pow(2, 53) + 13时,变成..110|1,1的前值为0,舍弃

  14. [h]当Math.pow(2, 53) + 14时,变成..111|0,舍弃尾部,值精确

  15. [h]当Math.pow(2, 53) + 15时,变成..111|1,1的前值为1,舍0进1,变成, .1000|0

  16. [h]当Math.pow(2, 53) + 16时,变成..1000|0,舍弃尾部,值精确
  17. [h]当Math.pow(2, 53) + 17时,变成..1000|1,1的前值为0,舍弃

  18. [i]当Math.pow(2, 53) + 18时,变成..1001|0,舍弃尾部,值精确

  19. [j]当Math.pow(2, 53) + 19时,变成..1001|1,1的前值为1,舍0进1,变成, .1010|0

  20. [j]当Math.pow(2, 53) + 20时,变成..1010|0,舍弃尾部,值精确

好了,列举了20个,你大概也看出规律了,也看明白了,就不多列了。

恒1

恒1法总得来说就是在舍去的尾部总是置位1,计算方法比较简单,可以自行上网搜索。