JS精度丢失问题

最近开发的项目新需求中,需要一个金额的输入框,其输入上限为16位整数带两位小数的浮点类型,由于前台显示的金额是字符串型,在传递给后台时使用了parseFloat()方法,却发现在测试校验最大临界值时,发生了四舍五入,最终导致校验失败,由此引发了我对JS浮点数表示和运算了深入理解。

JS浮点数精度丢失的原因

由于计算机的二进制实现和位数限制,有些数无法有限表示。就像一些无理数不能有限表示,如 圆周率 3.1415926…,1.3333… 等。JS 遵循 IEEE 754 规范,采用双精度存储(double precision),占用 64 bit。

Alt text

  • 1位用来表示符号位
  • 11位用来表示指数
  • 52位表示尾数

因为在计算机最底层,数值的运算和操作都是采用二进制实现的,所以计算机没有办法精确表示浮点数,而只能用二进制近似相等的去表示浮点数的小数部分。

1
2
0.1 >> 0.0001 1001 1001 1001…(1001无限循环)
0.2 >> 0.0011 0011 0011 0011…(0011无限循环)

当进行计算或其他操作时时,四舍五入(逢1进,逢0舍)将会导致最终的运算结果存在偏差。

而大整数也存在同样的问题,因为表示尾数的尾数只有52位,因此 JS 中能精准表示的最大整数是 Math.pow(2, 53),即十进制9007199254740992。

1
2
3
4
5
6
7
9007199254740992     >> 10000000000000...000 // 共计 53 个 0
9007199254740992 + 1 >> 10000000000000...001 // 中间 52 个 0
9007199254740992 + 2 >> 10000000000000...010 // 中间 51 个 0
9007199254740992 + 1 // 丢失 //9007199254740992
9007199254740992 + 2 // 未丢失 //9007199254740994
9007199254740992 + 3 // 丢失 //9007199254740992
9007199254740992 + 4 // 未丢失 //9007199254740996

由此可知,十进制中的有穷数值,在计算机底层,可能是0、1循环的无限数值。

在Java、C、C++中,均有对浮点数值的特殊处理,如Java的BigDecimal类型就是用来解决这一浮点数问题。

常见的出错场合

浮点数计算、比较:

1
0.1 + 0.2 != 0.3 // true

大整数计算、比较:

普通的整数计算比较不太容易出错,除非计算范围超出 Math.pow(2, 53)

1
9999999999999999 == 10000000000000001 // true

多位数字符数值转换:

这种情况在一些金额的计算中较容易出现,但也是最容易被忽视的一种,当用户在输入框中输入一个位数较多的字符串(不仅仅包含大数值,小数点后位数过长也包含在这一案例中),并在前台使用JS将其转换为数值,得到的结果往往是四舍五入带有偏差的值

1
2
3
4
parseFloat(0.9);    //0.9
parseFloat(9999999999999999.9) //10000000000000000
parseInt("9999999999999999"); //10000000000000000
parseFloat(9.999999999999999); //10

toFixed不会四舍五入:

1
2
var num = 1.335;
num.toFixed(2); //1.33

解决方案

浮点数计算、比较:

通常解决这一问题,采用的都是将浮点部分转换成整数后进行计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//浮点数转换为整数
function toInt(num){
var rel = {};
var str,pos,len,times;
str = (num < 0) ? -num + '' : num + '';
pos = str.indexOf('.');
len = str.substr(pos+1).length;
times = Math.pow(10, len);
rel.times = times;
rel.num = num;
return rel;
}

//计算过程
function operate(a,b,op){
var d1 = toInt(a);
var d2 = toInt(b);
var max = d1.times > d2.times ? d1.times : d2.times;
var rel;
switch(op){
case "+" :
rel = (d1.num * max + d2.num * max) / max;
break;
case "-" :
rel = (d1.num * max - d2.num * max) / max;
break;
case "*" :
rel = ((d1.num * max) * (d2.num * max)) / (max * max);
break;
case "/" :
rel = (d1.num * max) / (d2.num * max);
break;
}
return rel;
}

var rel = operate(0.3,0.1,"+"); //0.4

多位数数值转换:

前台不对这类字符串进行数值转换,传到后台后,由后台进行处理

toFix的修复:

1
2
3
4
5
6
function toFixed(num, s) {
var times = Math.pow(10, s)
var des = num * times + 0.5
des = parseInt(des, 10) / times
return des + ''
}