JS精度丢失问题(为什么0.1+0.2!==0.3)

244 阅读4分钟

❗️前端尽量不计算,特别是计算💰

看下面的例子,实际开发中,JS计算加减乘除都可能得到不精确值,发生我们常说的丢精度的问题。

console.log(0.1 + 0.2) // 0.30000000000000004
console.log(1.5 - 1.2) // 0.30000000000000004
console.log(19.9 * 100) // 1989.9999999999998
console.log(0.69 / 10) // 0.06899999999999999

我们按顺序来了解下面几个问题:

  • JS是怎么存储数字的
  • 计算的时候,JS是怎么读取数字的
  • 读取后,JS是如何计算的
(1)JS是怎么存储数字的

JS中数字遵循 IEEE 754标准,使用64位双精度浮点数来表示数字。

64位双精度浮点数,由3部分组成:

image.png

  1. 符号位S:1位,用来表示正负号。(1为负,0为正)
  2. 指数位E:11位,用来表示次方数。
  3. 尾数位M:52位,二进制有效数字,用来表示精确度。超过52位的部分自动进1舍0.

64位双精度浮点数,用二进制科学计数法表示,如下:

image.png
比如 8.25(十进制浮点数)

转换为`二进制浮点数`1000.01
使用`二进制科学计数法`表示:1.00001*(2^3)
解析变量:S=0, M=0.00001, E=1026(10000000010)

所以,存储为64位双精度浮点数:0(S)10000000010(E)00001(M) = 0100 0000 0010 0000 1000 ……(110000)

PS:十进制转二进制规则:整数除2取余,逆序排列;小数乘2取整,顺序排列。
(2)计算的时候,JS是怎么读取数字的
还是以 8.25 为例

内存存储的64位双精度浮点数:0100 0000 0010 0000 1000 ……(110000)
1、划分出SEM值:0(S)10000000010(E)00001(M)
2、转为二进制科学计数:
    (-1)^S = 1
    1 + M = 1.00001
    E=10000000010,转为10进制是2026
    E-2023 = 3

    计算为1.00001*2^3 = 1000.01
3、转为10进制浮点数:8.25
(3)JS是如何计算的
举个例子: 0.1 + 0.2 = 0.30000000000000004 

0.10.2存储为64位二进制浮点数如下图: 
0.1 => S=0,E=01111111011(1019),M=1001 10011001 10011001 10011001 10011001 10011001 10011010 (超出52位的丢弃,1001100110)  
0.2 => S=0,E=01111111100(1020),M=1001 10011001 10011001 10011001 10011001 10011001 10011010 (超出52位的丢弃,1001100110)   

读取两个数,并转为可以计算的二进制数: 
0.1=> 1.1001 10011001 10011001 10011001 10011001 10011001 10011010 * 2^(-4) => 0.00011001 10011001 10011001 10011001 10011001 10011001 10011010 
0.2=> 1.1001 10011001 10011001 10011001 10011001 10011001 10011010 * 2^(-3) => 0.0011001 10011001 10011001 10011001 10011001 10011001 10011010 

相加计算: 
0.1=>0.00011001100110011001100110011001100110011001100110011010 
0.2=>0.0011001100110011001100110011001100110011001100110011010   
  +=>0.0100110011001100110011001100110011001100110011001100111

计算得到的64位双精度浮点数,转为十进制浮点数,结果如图所示:

image.png

可以看出来,在JS存储的时候精度就丢失了,后面都是在不精准的值上计算的。

(4)如何让 0.1+0.2 == 0.3
  1. 使用 toFixed 方法对结果四舍五入
  2. 使用 Number.EPSILON 属性

Number.EPSILON:ES6引入的新属性,用于比较浮点数之间的精度。它表示比1大的最小浮点数与1之间的绝对差值。

if(Math.abs(0.1+0.2-0.3) < Number.EPLISON){ // 则判断 0.1+0.2 = 0.3}
  1. 使用社区提供一些成熟的库,比如 bignumber.jsdecimal.js,以及 big.js

因为一个bug,总结了这篇文章。如果有错误的话欢迎大家帮忙指正嗷!!!谢谢大家~~~