众所周知,JavaScript 浮点数运算时经常遇到会 0.0000000001和0.99999999这样奇怪的结果,如 1.07*10 = 10.700000000000001。但是,往往当我们遇到问题之后才想起来浮点数对我们造成的负面影响,如果能提前避免,不是更好吗?
常见的几种场景
场景一:进行浮点值运算结果的判断
常见错误写法:floatNum1 + floatNum2 === res
我们在Chrome里测试一下 0.1 + 0.2 === 0.3,得出的结果是false,而不是预期结果true,因为 0.1 + 0.2 === 0.30000000000000004
场景二:将小数乘以10的n次方取整(比如将元转化成分)
常见错误写法:parseInt(yuan*100, 10)
我们在Chrome里测试一下 parseInt(0.58*100, 10),得出的结果是57,而不是预期结果58。
场景三: 四舍五入保留n位小数
常见错误写法: (number).toFixed(2)
我们在Chrome里测试一下 (1.335).toFixed(2) ,得出的结果是1.33,而不是预期结果1.34。
可以发现,稍有不注意,浮点数运算就会出问题,那么我们如何解决、防范以上问题呢?
常见解决方案
因为大家一旦察觉到浮点数精度的负面影响,就会马上想到不同思路的解决方案,因此本文的目的是希望大家引起对浮点数的重视,尤其是在计算金额这种需要高严谨数据的枪框下,而非重点介绍解决方案,所以,这一部分就不详细展开了。
大概就是将浮点数转化成字符串,通过String.split取出小数点前后的数据再做特殊处理。当然也有现成的库可供选择,如 number-precision。
那么下面我们详细了解一下为什么会出现这种奇怪的现象呢?
浮点数的存储
JavaScript 中所有数字包括整数和小数都只有一种类型 — Number。它的实现遵循 IEEE 754 标准,使用 64 位固定长度来表示,也就是标准的 double 双精度浮点数(相关的还有float 32位单精度)。
这样的存储结构优点是可以归一化处理整数和小数,节省存储空间。64位比特又可分为三个部分:符号位S:第 1 位是正负数符号位(sign),0代表正数,1代表负数
指数位E:中间的 11 位存储指数(exponent),用来表示次方数
尾数位M:最后的 52 位是尾数(mantissa),超出的部分自动进一舍零
实际数字就可以用以下公式来计算:
注意以上的公式遵循科学计数法的规范,在十进制中 0
不知道怎么转换的同学可以查看 二进制十进制间小数怎么转换 。
用二进制对应的科学计数法就是 1.001*2^2,舍去1后M=001。
因为指数位E有 11 位,是一个无符号整数,取值范围是 0 到 2047(2047 = Math.pow(2,11)-1)。但是科学计数法中的指数是可以为负数的,所以约定减去一个中间数 1023,[0,1022] 表示为负,[1024,2047] 表示为正。如 4.5 的指数 E = 1025(1025 = 1023+2),尾数 M = 001。
最终的公式变成:
所以 4.5 最终表示为(S=0、M=001、E=1025)
4.5二进制
下面再以 0.1 为例解释浮点误差的原因,0.1 转成二进制表示为 0.0001100110011001100(1100循环),1.100110011001100x2^-4,所以 E=-4+1023=1019;M 舍去首位的1,得到 100110011...。最终就是:
0.1二进制
转化成十进制后为 0.100000000000000005551115123126,因此就出现了浮点误差。
那么下面我们再回到应用场景中的例子,通过二进制数据查看究竟为什么会出现问题:
为什么0.1+0.2=0.30000000000000004?
// 0.1 和 0.2 都转化成二进制后再进行运算
0.00011001100110011001100110011001100110011001100110011010 +
0.0011001100110011001100110011001100110011001100110011010 =
0.0100110011001100110011001100110011001100110011001100111
// 转成十进制正好是 0.30000000000000004
为什么(1.335).toFixed(2)=1.33?
因为1.335其实是1.33499999999999996447286321199,toFixed虽然是四舍五入,但是是对1.33499999999999996447286321199进行四五入,所以得出 1.33。
小结
关注浮点数计算,防患于未然。