2

先看如下计算的输出:

0.1 + 0.2

显然是0.3。但是在javascript中,结果是什么呢?

0.30000000000000004

这是程序语言在数值计算中很容易出现的精度问题,如下图饿了么账单页金额显示。

问题产生的原因

先来看对Number类型数值二进制的表示,由3部分组成:

符号位 * 指数位 * 尾数位

由于js采用64位双精度浮点数编码,实际存储时为了节省空间,采用科学计数法表示,其二进制构成如下:

符号位占1位,指数位占11位,尾数占52位。

问题分解

0.1的二进制表示为:

0.0001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 ...

其科学计数法表示为:

1.1001... * 2^-4

其中指数位采用偏置码处理,-4即为:01111111011。简单介绍下(自行百度):

双精度采用的偏置码为1023,
比如指数位:01111111011,其值为1019,
1019 - 1023 = -4

由于尾数位仅为52位,因此需要截取前52位,并且如若第53位为1则进1,反之舍去,因此0.1的尾数位截取后为:

//10011001 10011001 1001100 110011001 10011001 10011001 10011001...
//由于53位为1,进1,即为:
10011001 10011001 10011001 10011001 10011001 10011001 1010

可以看到0.1的值其实已经不准确了,较原值偏大。其对应的二进制存储表示如下:

有的童鞋可能注意到了,尾数位存储的是小数部分,这是因为规格化后的值通式为1.x,因此可以略去1,节省了一个bit位空间。

同理0.2的二进制如下:

0.001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001...

科学计数法处理后的二进制存储为:

至此,已经清楚了javascript对数值的存储方式。

进制转换网址参见:http://www.binaryconvert.com/

0.3的二进制表示为:

0.010011001100110011001100110011001100110011001100110011...

使用科学计数法表示后存储为:

而计算机在处理0.1+0.2时(上面已经知道了其分别对应的二进制存储方式),需要通过对阶、尾数求和、规格化、舍入等操作(这里不再赘述),最终得到:

0.010011001100110011001100110011001100110011001100110100
//转为10进制即为:0.30000000000000004

可以知道,计算机在进行浮点数加减运算时,包括对阶、规格化过程都可能产生精度误差,核心还是因为尾数位的位数有限,1进0舍导入的误差。

如何在开发中避免此类问题?

1. 取固定精度

有的童鞋可能会采用toFixed()获取固定精度,如下

(0.1+0.2).toFixed(1) = 0.3;

对于精度要求不高的话,这种通过4舍5入获取固定精度的方式一般可以满足需求。

2. 先将小数转为整数再进行计算

0.1 + 0.2
//将两者都转化为整数的最小公倍数:RATE = 10
(0.1*RATE + 0.2*RATE)/RATE
= 0.3

这是日常开发中最常用的方式,推荐。

扩展

如果清楚上面讲解的数值存储方式,那么可以知道js的安全整数范围为:

Math.pow(2, 53) - 1  
// 可表示的安全整数范围:
// Number.MIN_SAFE_INTEGER ~ Number.MAX_SAFE_INTEGER
-9007199254740991 ~ 9007199254740991

超出这个范围的整数计算会出现精度丢失问题。

需要处理较大值的话,可以参考bignumber.js等;另外ES2020,加入了BigInt类型:

let number1 = BigInt(123);  //方式1
let number2 = 123n;  //方式2
number1 == number2;  //true
typeof number1; //"bigint"

谷歌浏览器已经支持了,可以尝试下~

获取更多干货分享,请【扫码关注】~
head.png


夜暮sky
97 声望5 粉丝