# JS精度问题
我们大家都知道,JS有个很经典的浮点运算精度丢失问题,今天我们就来聊一聊这个问题产生的原因,以及该如何去解决它呢?
先来看下面的代码,0.1+0.2的结果不等于0.3,这是不是超出了我们之前的认知呢?毕竟0.1+0.2=0.3可是我们小学就已经学会了的东西,到这里怎么就不一样了呢?
0.1 + 0.2 //0.30000000000000004
下面让我们先来了解它是如何产生的,然后再去解决它.
# 为什么产生
首先,我们要知道数字在计算机中是如何存储和运行的?在计算机中,数字无论是定点数还是浮点数都是以多位二进制的方式进行存储.js采用IEEE 754的双精度标准进行存储,这是一种64位双精度浮点数储存方法.其中1
位表示符号位,有正负,0为正,1为负;11
位用来表示指数,剩下的52
位表示尾数.
它的表示格式为:
(s) * (m) * (2 ^ e)
s为符号位,m为尾数,e为指数.
ES6在Number
对象上新增了一个极小的常量Number.EPSILON
.根据规格,它表示1与大于1的最小浮点数之间的差.我们在控制台打印出它的结果,可以看到
Number.EPSILON //2.220446049250313e-16
前面说到了64位浮点数中有52位是表示精度,那么比1大的最小浮点数应该就是1.000..001
,这里小数点后面有51个0,然后1个1.这个数减去1的结果就是2的-52次方,也就是Math.pow(2,-52)
,所以下面的结果会输出true
Number.EPSILON === Math.pow(2,-52) //true
所以,我们可以认为 Number.EPSILON
是JS中能够表示的最小精度.当误差小于这个值的时候,就已经没有意义了,可以认为此时误差已经不存在了.即如果两个浮点数之间的差小于Number.EPSILON
,则我们认为这两个浮点数是相等的.
回过头来,我们来计算一下0.1+0.2为啥不等于0.3?
将十进制的小数转换为二进制的小数,我们采用的是乘2取整法
.即将小数部分乘以2,然后取整数部分,剩下的小数部分继续乘以2,然后取整数部分,剩下的小数部分又乘以2,一直取到小数部分为零为止.
我们先将0.1转换成二进制,其结果为0.000110011..001
其中小数部分从第2位开始就是0011一直循环.我们看似有穷的一个小数0.1,其实在计算机中是无穷的.由于存储空间有限,计算机会舍弃掉后面的数值.
我们再将0.2转换成二进制,其结果为0.00110011..001
其中小数部分从第1位开始就是0011一直循环.
在这里提供一个在线进制转换的网址戳我.
然后我们用js中的IEEE 754 双精度64位浮点数表示法来展示0.1和0.2,其结果为:
十进制小数 | 指数e | 尾数m |
---|---|---|
0.1 | -4 | 1.1001100110011001100110011001100110011001100110011010(52位) |
0.2 | -3 | 1.1001100110011001100110011001100110011001100110011010(52位) |
然后,我们把他们相加,这里指数不一样的话,我们选择右移,因为损失的精度小.
十进制小数 | 指数e | 尾数m |
---|---|---|
0.1 | -3 | 0.1100110011001100110011001100110011001100110011001101(52位) |
0.2 | -3 | 1.1001100110011001100110011001100110011001100110011010(52位) |
-3 | 10.0110011001100110011001100110011001100110011001100111 (52位) |
e=-3;m=0.1100110011001100110011001100110011001100110011001101(52位)
+
e=-3;m=1.1001100110011001100110011001100110011001100110011010(52位)
结果是:
e=-3;m=10.0110011001100110011001100110011001100110011001100111 (52位)
即:
e=-2;m=1.00110011001100110011001100110011001100110011001100111 (53位)
可以看到,这时尾数已经有53位了,我们采用一个叫round to nearest, tie to even
四舍五入的方式.它的意思就是接近哪个取哪个,一样的时候取偶数.举个例子:1.0101保留3位小数,那么它可以是1.010和1.011,此时取哪个,取偶数1.010.所以,这里e=-2;m=1.00110011001100110011001100110011001100110011001100111 (53位)
转换为e=-2;m=1.0011001100110011001100110011001100110011001100110100 (52位)
,其二进制小数表示法就是0.010011001100110011001100110011001100110011001100110100
,我们再将其展示为十进制的小数,结果为0.30000000000000004
.此结果可以去上面提供的进制转换网站做验证.自此,我们就明白了为什么0.1+0.2 != 0.3
.
# 怎么解决
既然已经知道了导致这个问题的原因,那么我们该如何解决呢?
# 使用函数库
常见的函数库,比如decimal.js等就可以解决这个问题
# 自己写函数
知道了问题出在哪儿,我们也就有了解决思路.通常我们的做法是将浮点数变成整数来计算,然后再确定小数点的位置,下面的加法函数就实现了我们想要的结果.
function add(num1, num2){
let r1, r2, m;
try{
r1 = num1.toString().split('.')[1].length
}catch(e){
r1 = 0
}
try{
r2 = num2.toString().split('.')[1].length
}catch(e){
r2 = 0
}
m = Math.pow(10, Math.max(r1, r2))
return (num1 * m + num2 * m) / m
}
相类似的, 还有减乘除的函数,我们也一并展示在下面:
减法函数:
function sub(num1, num2){
let r1, r2, m, n;
try{
r1 = num1.toString().split('.')[1].length
}catch(e){
r1 = 0
}
try{
r2 = num2.toString().split('.')[1].length
}catch(e){
r2 = 0
}
n = Math.max(r1, r2)
m = Math.pow(10, n)
return Number(((num1 * m - num2 * m) / m).toFixed(n))
}
乘法函数:
function multiply(num1, num2){
let m = 0,
s1 = num1.toString(),
s2 = num2.toString()
try {
m += s1.split('.')[1].length
}catch(e){}
try {
m += s2.split('.')[1].length
}catch(e){}
return Number(s1.replace('.','')) * Number(s2.replace('.','')) / Math.pow(10,m)
}
除法函数:
function divide(num1, num2){
let t1,t2,r1,r2;
try {
t1 = num1.toString().split('.')[1].length
}catch(e){
t1 = 0
}
try {
t2 = num2.toString().split('.')[1].length
}catch(e){
t2 = 0
}
r1 = Number(num1.toString().replace('.',''))
r2 = Number(num2.toString().replace('.',''))
return (r1 / r2) * Math.pow(10, t2 - t1)
}
以上的这些函数分别可以解决加减乘除精度显示的问题.
← JS简介 ES6 Set 数据结构 →