Skip to content

浮点数精度问题

前置知识

在 JavaScript 中,小数的运算是精确的吗?

不一定。

在 JavaScript 中,整数是连续的吗?

不一定,当数字大到一定程度就不是连续的。

在 JavaScript 中,整数的运算是精确的吗?

不一定。

在 JavaScript 中,最大连续整数是多少?

通过 Number.MAX_SAFE_INTEGER就获取最大连续整数。

在 JavaScript 中,能表示数字的有效位数是多少?

表示整数的有效位数是16位,表示小数的有效位数是16 ~ 17 位。

表示数字的方式

在现实世界中,用十进制表示数字,也就是有10个数字(0~9),逢十进一。

在计算机世界中,用二进制表示数字,也就是有2个数字(0~1),逢二进一。

要想知道十进制数字如何用二进制来表示,或二进制数字如何用十进制来表示,就涉及到了它们之间的互相转换。

二进制 -> 十进制

例1:将二进制数1101转换为十进制数。

1101 = 123+122+021+120=13

例2:将二进制数11.01转换为十进制数。

11.01 = 121+120+021+122=3.25

十进制 -> 二进制

例1:将十进制数13转换为二进制数。

13 / 2    商 6   余 1
6  / 2    商 3   余 0
3  / 2    商 1   余 1
1  / 2    商 0   余 1  (商为0时停止整除)

余数从下往上读,十进制数13转换为二进制数的结果为1101

例2:将十进制数3.25转换为二进制数。

将3.25分为整数3和小数0.25

整数部分一样
3 / 2    商 1   余 1
1 / 2    商 0   余 1
余树从下往上读,整数3转为二进制结果为11

小数部分
0.25 * 2   0.5  整数:0,小数5
0.5  * 2   1.0  整数:1,小数0   (小数为0时停止相乘)
整数从上往下读,小数0.25转为二进制结果为0.01

最终结果为11.01

经典问题:为什么0.1 + 0.2 === 0.3为 false?

这是因为在现实世界中我们用十进制来表示数字,而计算机世界中用二进制表示数字。当我们在计算机中输入0.1、0.2时,计算机需要将它们转换为二进制再进行计算。

但十进制的小数转换为二进制的时候,可能会是无限小数。又因为计算机对数字的存储能力有限,只能丢失一些数据,这就导致小数的精度可能会出现偏差。

例:将十进制数0.3转换为二进制数。

0.3 * 2   0.6  整数:0
0.6 * 2   1.2  整数:1
0.2 * 2   0.4  整数:0
0.4 * 2   0.8  整数:0
0.8 * 2   1.6  整数:1
0.6 * 2   1.2  整数:1  (从这里开始出现无限小数,导致小数永远无法为0)
0.2 * 2   0.4  整数:0
0.4 * 2   0.8  整数:0
0.8 * 2   1.6  整数:1
......
整数从上往下读
最终结果为0.0100110011001......

如何解决小数运算精度问题?

将小数化为整数进行计算。

js
// 精确加法
function add(num1, num2) {
  const num1Digits = (num1.toString().split('.')[1] || '').length;
  const num2Digits = (num2.toString().split('.')[1] || '').length;
  const baseNum = Math.pow(10, Math.max(num1Digits, num2Digits));
  return (num1 * baseNum + num2 * baseNum) / baseNum;
}

若要解决 toFixed 四舍五入的问题,可使用第三方库 lodash.js 中的 round 方法。

JavaScript 如何存储数字?

存储数字的方式通常是整数法和浮点法,其中整数法存储整数,浮点法存储小数。

但 JavaScript 中所有的数字都采用浮点法来存储。

浮点法存储的小数叫做浮点数(float),浮点数分为单精度和双精度。

JavaScript 采用的是双精度来存放浮点数,它的实现遵循 IEEE 754 标准

存储方式

JavaScript 在计算机中,给每个数字开辟一块内存空间,尺寸固定为64位。

在计算机中,位(bit)是最小的存储单位。

1bit = 8 byte,1KB = 1024 byte,1MB = 1024 KB,1GB = 1024 MB

在内存空间中,这64位是这样分配的。

[第1段][第2段][第3段]

第1段:第1位,表示符号位,如果为1,是负数,如果为0,是正数
第2段:第2-12位,共11位,表示指数位,这里的指数是2为底的指数,而不是10
第3段:第13-64位,共52位,表示尾数,相当于小数部分

指数位如何表示负数?

指数位一共有11位,能够表示的数字有211=2048个,即0~2047。

但是为了能够表示负数,需要将指数位转换为十进制后再减去1023,得到的数字就是最终指数位的十进制数。

例:

0    1000 0000 010    0100 0000 0000 0000....

转换为十进制如下:

第1段 0,表示正数。

第2段100 0000 0010,表示指数,即1210+121=102610261023=3

第3段0100 0000 0000 0000....,表示尾数,整数部分默认为1,因此需要将二进制数1.0100 0000 0000 0000....转换为十进制数,即120+122=1.25

整合起来,即1.2523=10

特殊情况

  1. 指数为0,尾数为0,表示数字 0
  2. 符号为0,指数为2047,尾数为0,表示正无穷
Infinity: 0 11111111111 00000000000...(尾数全是0)
  1. 符号为1,指数为2047,尾数为0,表示负无穷
-Infinity: 1 11111111111 00000000000...(尾数全是0)
  1. 指数为2047,尾数不为0,表示NaN
NaN: 1 11111111111 01001010000...(尾数任意)

根据上面的特殊情况,我们可以得出:

  • 一个正常的数字,指数部分最大为 11111111110,即2046。指数为2047的时候一定是一个特殊值。
  • 能表示的最大数字:
0 11111111110 1111111111.........(尾数全是1)

1.11111....21023,等于 Number.MAX_VALUE

  • 能表示的最大安全数字:

安全数字:从1开始到该数字均是连续的整数,并且该数字的下一个整数也是存在的。

0 (暂时忽略指数部分) 1111111111....(尾数全是1)

1.1111...(52)2

这时候再来看指数部分,如果指数部分转换为十进制数是52,那么正好能将1.1111...(52位小数)变为整数,即11111....(53位二进制数)。这53位全是1的二进制数相当于 $2^{53} -1 $。

例:

十进制数中,最大的数字是9,那么99相当于1021999相当于1031,9999相当于1041

同理二进制数中,最大数字是1,53位二进制数相当于$2^{53} -1 $。

因此,最大安全数字是2531=9007199254740991等于Number.MAX_SAFE_INTEGER。在控制台中用这个数字加一,可以看到能够正常显示。

如何解决大数运算不精确的问题?

将大数转为字符串,重新实现运算逻辑,再转位数字。

这里推荐使用第三方库bignumber.js

总结

0.1 + 0.2 === 0.3为 false 是因为浮点数精度不准确。我们在现实世界中用十进制表示数字,在计算机世界中用二进制表示数字。因此我们在计算机中输入 0.1 + 0.2 时,计算机需要将它们转换为二进制数再进行计算。但十进制的小数转换为二进制的时候,可能会是无限小数。又因为计算机对数字的存储能力有限,只能丢失一些数据,这就导致小数的精度可能会出现偏差。

JavaScript 存储数字遵循的是 IEEE 754 标准,采用双精度64位的方式存储。

在这64位中,分为三段:

第一段,只有一位,0表示整数,1表示负数。

第二段,第2~12位,共11位,表示指数。

第三段,第12~64位,共52位,表示尾数。

1、为什么小数运算不精确?

因为部分十进制数转换为二进制数时会出现无限小数,而 JavaScript 在计算机中存储数字的内存有限(64位),无法存储具有无限小数的数字,因此只能丢失部分数据,这就导致最终小数运算不精确。

2、为什么整数可能不连续?

因为 JavaScript 存在最大安全数字,超过这个数字就可能不是连续的。

3、 为什么整数运算不精确?

同2,如果这个数字是最大连续整数的下一位,那么使用它来运算就可能会出现不精确的情况。

4、最大连续整数

也就是最大安全数字,指的是从1到该数均为连续整数,并且该数的下一个整数也是存在的,可通过 Number.MAX_SAFE_INTEGER查看。

5、最大有效位数

表示整数的有效位数是16位,表示小数的有效位数是16 ~ 17 位,超过将会丢失精度。

例:在最大安全数后加几个数字可以看到都变成了0,这就是因为超过有效位数而丢失精度导致的。