浮点数精度问题
前置知识
在 JavaScript 中,小数的运算是精确的吗?
不一定。
在 JavaScript 中,整数是连续的吗?
不一定,当数字大到一定程度就不是连续的。
在 JavaScript 中,整数的运算是精确的吗?
不一定。
在 JavaScript 中,最大连续整数是多少?
通过 Number.MAX_SAFE_INTEGER就获取最大连续整数。
在 JavaScript 中,能表示数字的有效位数是多少?
表示整数的有效位数是16位,表示小数的有效位数是16 ~ 17 位。
表示数字的方式
在现实世界中,用十进制表示数字,也就是有10个数字(0~9),逢十进一。
在计算机世界中,用二进制表示数字,也就是有2个数字(0~1),逢二进一。
要想知道十进制数字如何用二进制来表示,或二进制数字如何用十进制来表示,就涉及到了它们之间的互相转换。
二进制 -> 十进制
例1:将二进制数1101转换为十进制数。
1101 =
例2:将二进制数11.01转换为十进制数。
11.01 =
十进制 -> 二进制
例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......如何解决小数运算精度问题?
将小数化为整数进行计算。
// 精确加法
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位,能够表示的数字有
但是为了能够表示负数,需要将指数位转换为十进制后再减去1023,得到的数字就是最终指数位的十进制数。
例:
0 1000 0000 010 0100 0000 0000 0000....转换为十进制如下:
第1段 0,表示正数。
第2段100 0000 0010,表示指数,即
第3段0100 0000 0000 0000....,表示尾数,整数部分默认为1,因此需要将二进制数1.0100 0000 0000 0000....转换为十进制数,即
整合起来,即
特殊情况
- 指数为0,尾数为0,表示数字 0
- 符号为0,指数为2047,尾数为0,表示正无穷
Infinity: 0 11111111111 00000000000...(尾数全是0)- 符号为1,指数为2047,尾数为0,表示负无穷
-Infinity: 1 11111111111 00000000000...(尾数全是0)- 指数为2047,尾数不为0,表示NaN
NaN: 1 11111111111 01001010000...(尾数任意)根据上面的特殊情况,我们可以得出:
- 一个正常的数字,指数部分最大为
11111111110,即2046。指数为2047的时候一定是一个特殊值。 - 能表示的最大数字:
0 11111111110 1111111111.........(尾数全是1)即 Number.MAX_VALUE
- 能表示的最大安全数字:
安全数字:从1开始到该数字均是连续的整数,并且该数字的下一个整数也是存在的。
0 (暂时忽略指数部分) 1111111111....(尾数全是1)即
这时候再来看指数部分,如果指数部分转换为十进制数是52,那么正好能将1.1111...(52位小数)变为整数,即11111....(53位二进制数)。这53位全是1的二进制数相当于 $2^{53} -1 $。
例:
十进制数中,最大的数字是9,那么99相当于999相当于
同理二进制数中,最大数字是1,53位二进制数相当于$2^{53} -1 $。
因此,最大安全数字是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,这就是因为超过有效位数而丢失精度导致的。