toFixed 函数 bug -- "0.1+0.2 等于 0
来看一下toFixed()在chrome、火狐、IE下的不同表现。
chrome:
![](https://img.haomeiwen.com/i5602642/fc544ad31ee61964.png)
火狐:
![](https://img.haomeiwen.com/i5602642/3a840a617f6566d0.png)
IE:
![](https://img.haomeiwen.com/i5602642/072544890b4be2e8.png)
可以看到toFixed()的四舍五入在chrome、火狐上并不准确。
而toFixed()在chrome、火狐上也并不是网上所说的用银行家舍入法来进行四舍五入的。
银行家舍入法的规则是“四舍六入五考虑,五后非零就进一,五后为零看奇偶,五前为偶应舍去,五前为奇要进一”。
例如银行家舍入法在 (2.55).toFixed(1) = 2.5、(3.55).toFixed(1) = 3.5 上就不符合了。
那为什么会这样呢,要从toFixed的定义说起,来看ecmascript 规范对toFixed的表述:
![](https://img.haomeiwen.com/i5602642/8c57b216ecb144c2.png)
按上图中的步骤来演示一下(2.55).toFixed(1) = 2.5的处理过程。
![](https://img.haomeiwen.com/i5602642/5e6d9e18b256ba40.png)
可以看到最接近零的应该是 -0.04999... ,故n为25,那么m为25,k为2, 为1,故a为2,则b为5,所以(2.55).toFixed(1)的结果为2.5。可以看出(2.55).toFixed(1)
的结果是2.5
而不是2.6
,是... 引起,而为什么不等于0.5,其原因和不等0.3是一样
0.1+0.2 的计算过程计算过程
1、十进制转成二进制
在JS内部所有的计算都是以二进制方式计算的。 所以运算 0.1+ 0.2 时要先把 0.1和 0.2 从十进制转成二进制。
- 0.1转化成二进制的算法:
0.12=0.2======取出整数部分0
0.22=0.4======取出整数部分0
0.42=0.8======取出整数部分0
0.82=1.6======取出整数部分1
0.62=1.2======取出整数部分1
接下来会无限循环
0.22=0.4======取出整数部分0
0.42=0.8======取出整数部分0
0.82=1.6======取出整数部分1
0.6*2=1.2======取出整数部分1
所以0.1转化成二进制是:0.0001 1001 1001 1001...... - 0.2转化成二进制的算法:
0.22=0.4======取出整数部分0
0.42=0.8======取出整数部分0
0.82=1.6======取出整数部分1
0.62=1.2======取出整数部分1
接下来会无限循环
0.22=0.4======取出整数部分0
0.42=0.8======取出整数部分0
0.82=1.6======取出整数部分1
0.62=1.2======取出整数部分1
所以0.2转化成二进制是:0.0011 0011 0011 0011......
这里要注意 0.1 和 0.2 转成的二进制是无穷的。另外在现代浏览器中是用浮点数形式的二进制来存储二进制,所以还要把上面所转化的二进制转成浮点数形式的二进制。
2、转成浮点数
浮点数分为单精度对应32位操作系统和双精度对应64位操作系统。目前的操作系统大多是64位操作系统,故这里只解释一下二进制如何转成双精度浮点数的二进制。
双精度浮点数用1位表示符号位,11位表示指数位,52位表示小数位,如下图所示:
![](https://img.haomeiwen.com/i5602642/92ca8faa786c4e07.png)
-
符号位:正数为0,负数为1;
-
指数位:
阶数
+偏移量
,阶数是:图片
,e为阶码的位数。偏移量是把小数点移动到整数位只有1时移动的位数,正数表示向左移,负数表示向右移;
-
小数位:即二进制小数点后面的数。
接下来把0.1转成的二进制0.0001100110011001 ......转成浮点数形式的二进制。
-
先要把小数点移动到整数位只有1,要向右移动4位,故偏移量为−4,通过指位数的计算公式
图片
,把1019转成二进制为1111111011,不够11位要补零,最终得出指位数为01111111011;
-
小数位为100110011001...... ,因为小数位只能保留52位,第53位为1故进1。
转换结果如下图所示:
![](https://img.haomeiwen.com/i5602642/eb00c113a1564d34.png)
同理,再把 0.2 转成的二进制0.0011 0011 0011 0011...... 转成浮点数形式的二进制,转换结果如下图所示:
![](https://img.haomeiwen.com/i5602642/9df652c2e8b44615.png)
浮点数相加
浮点数相加时,需要先比较指位数是否一致,如果一致则小数位直接相加,如果不一致,要先把指位数调成一致的,指位数小的向大的调整。
为了行文方便,把0.1转成的浮点数称为为0.1,把0.2转成的浮点数称为0.2。
0.1的指数位是1019 ,0.2的指数位是1020 。故要把0.1的指数位加1,即把0.1的小数点向左移动1位,另外浮点数的整数位固定为1,过程如下所示
1.1001100110011001100110011001100110011001100110011010 原先
0.11001100110011001100110011001100110011001100110011010 移动后
0.1100110011001100110011001100110011001100110011001101 将小数的第53位舍去,因为为0故不需进1
导致0.1的小数位变成如下所示:
![](https://img.haomeiwen.com/i5602642/d4625530878c9acb.png)
现在0.1和0.2的指数位相同了,把小数位直接相加。
1100110011001100110011001100110011001100110011001101 0.1的小数位
+ 1001100110011001100110011001100110011001100110011010 0.2的小数位
= 10110011001100110011001100110011001100110011001100111
会发现现在的小数位多出了一位,超出了52位,故要把小数位最后一位截掉,小数位最后一位是1,故要进1,如下所示:
10110011001100110011001100110011001100110011001100111
1011001100110011001100110011001100110011001100110100
截掉小数位的最后一位相当把小数点向左移了一位,故指数位要加1,此时的指数是0.2的指数1021 ,加1后变成1021 ,转成二进制为01111111101 ,那么相加后的浮点数如下所示:
![](https://img.haomeiwen.com/i5602642/9c79a6678aea897d.png)
浮点数转成十进制
![](https://img.haomeiwen.com/i5602642/870f646f0ea95c1e.png)
结果如下所示:
0.3000000000000000444089209850062616169452667236328125
由于精度问题,只取到0.30000000000000004。
0.1+0.2 不等于 0.3 ,因为在 0.1+0.2 的计算过程中发生了两次精度丢失。第一次是在 0.1 和 0.2 转成双精度二进制浮点数时,由于二进制浮点数的小数位只能存储52位,导致小数点后第53位的数要进行为1则进1为0则舍去的操作,从而造成一次精度丢失。第二次在 0.1 和 0.2 转成二进制浮点数后,二进制浮点数相加的过程中,小数位相加导致小数位多出了一位,又要让第53位的数进行为1则进1为0则舍去的操作,又造成一次精度丢失。最终导致 0.1+0.2 不等于0.3 。
但是在IE浏览器中,执行 和 的结果和在chrome和火狐浏览器中执行的结果是一样。这里只能推断IE浏览器中定义的toFixed不符合ecmascript 规范
解决
假设要四舍五入的数字为number,要保留n位小数,可以先用![](https://img.haomeiwen.com/i5602642/3e668b9f1d46661a.png)
,然后用 Math.round()取整,最后在除去,间接实现四舍五入。另外toFixed()还有个自动补零的功能,也要实现一下,故下面简单封装了一个toFixed方法来实现四舍五入。
function toFixed(number, m) {
if (typeof number !== 'number') {
throw new Error("number不是数字");
}
let result = Math.round(Math.pow(10, m) * number) / Math.pow(10, m);
result = String(result);
if (result.indexOf(".") == -1) {
if(m != 0){
result += ".";
result += new Array(m + 1).join('0');
}
} else {
let arr = result.split('.');
if (arr[1].length < m) {
arr[1] += new Array(m - arr[1].length + 1).join('0')
}
result = arr.join('.')
}
return result
}