先看如下计算的输出:
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"
谷歌浏览器已经支持了,可以尝试下~