number.toFixed和Math.round与保留小数
如果你baidu/google过或者自己写过保留两位小数,那下面这代码一定不陌生
Math.round(number*100)/100
那你使用过Number.prototype.toFixed这个方法吗。老实说此前我一次没用过,我猜我以前看书的时候没注意它(反省img...)。
今天看书复习再次看到这个方法,感觉很方便的,一个方法搞定保留小数,岂不是美滋滋。
研究以后发现事情并没有那么简单。
根据网上的说法,toFixed使用的是银行家舍入规则。并非我们熟悉的四舍五入,所以并不适合用来保留小数。对于银行家舍入的解释引用自互联网:
银行家舍入:所谓银行家舍入法,其实质是一种四舍六入五取偶(又称四舍六入五留双)法。
简单来说就是:四舍六入五考虑,五后非零就进一,五后为零看奇偶,五前为偶应舍去,五前为奇要进一。
规则不难理解,可不知道为什么我就是想去浏览器控制台试试....
说好的银行家舍入呢(手动滑稽)。
到这里我开始自己对问题的猜想,是不是以前toFiexed舍入规则是这种银行家舍入规则,而后来随着版本的变更,方法已经变为我们熟知的四舍五入了。现在到底能不能用,我决定试试
function contrast(number,fractionDigits) { var times = Math.pow(10,fractionDigits); var roundNum = Math.round(number*times)/times; var toFixedNum = number.toFixed(fractionDigits); if(roundNum.toString() !== toFixedNum){ console.log('-------------------------'); console.count(); console.log("number:"+number); console.log("fractionDigits:"+fractionDigits); console.log("roundNum:"+roundNum); console.log("toFixedNum:"+toFixedNum); } } var number, fractionDigits; for (var i = 0; i < 1000; i++) { number = Math.floor(Math.random()*Math.pow(10,16))/Math.pow(10,16); //一开始是以toFiexed参数范围取的20位,后来发现精度只支持16位 fractionDigits = Math.floor(Math.random()*16); contrast(number,fractionDigits) }
实验的结果,有两种情况,一是值一样,保留的小数不同(末尾0舍去与否)
坏就坏在第二种情况
出问题的数,保留小数,都在15位,难道是支持的准确精度位数不够吗,并不是的,比如:
这图也可以证明,toFiexed并非什么银行家舍入规则,至少我现在使用的chrome 62.0.3202.75不是,我使用的6.10.3nodejs也不是
网上找不到答案,那就书上找呗。在犀牛书里3.1.4里找到了一个原因,二进制浮点数表示法:
此外在知乎看到一个相当典型的例子1.555+1
可见1.555,在js里,其实是一个非常接近真实的1.555,但小于它的一个近似值。
我们认为的那个该进一的5其实在计算机眼中是4999..由上面的结果,不难预见到下面的结果
由此可见真正的问题源于二进制浮点数表示法并不能精准表示十进制分数!
----------------------------------------------分割线----------------------------------------------
理论完了,下面说说应用。
虽然toFixed由于二进制浮点数表示法的精确问题,并不能成为可靠的保留小数方案。
但我注意到其对小数位数的保留比round实现的保留小数(未做补零处理前)位数准确,也就是实验中的第一种情况。
我想利用这一特点,来给round保留的小数补零!!
由于Math.round(number*times)/times正确处理过toFiexed可能出错的5(4999....),这时候再用toFiexed,就可以避免错误(因为现在是999....\000...)。而又可以利用toFiexed正确保留小数位数的特点来补零。
function contrast(number,fractionDigits) { var times = Math.pow(10, fractionDigits); var roundNum = Math.round(number * times) / times; var toFixedNum = number.toFixed(fractionDigits); var decimal = roundNum.toString().split("."); var realValue1 = roundNum.toString();//手动补0 var realValue2 = roundNum.toFixed(fractionDigits);//toFixed补0 if(decimal.length === 2 ){ if (decimal[1].length < fractionDigits) { realValue1 = decimal[0] + '.' + (decimal[1] + "0000000000000000").slice(0,fractionDigits); } }else if(fractionDigits !== 0){ realValue1 = decimal[0] + '.' + ("0000000000000000").slice(0,fractionDigits); } if (realValue1 !== realValue2) { console.log('-------------------------'); console.count('错误数'); console.log("number:" + number); console.log("fractionDigits:" + fractionDigits); console.log("roundNum:" + roundNum); console.log("realValue1:" + realValue1); console.log("realValue2:" + realValue2); console.log("toFixedNum:" + toFixedNum); }else{ console.count('正确数'); } } var number, fractionDigits; for (var i = 0; i < 10000; i++) { number = Math.floor(Math.random()*Math.pow(10,20))/Math.pow(10,20); fractionDigits = Math.floor(Math.random()*16); contrast(number,fractionDigits) }
实验了10000*10次随机数随机保留小数,无错误。
理论和实际都无问题,以后我保留小数就准备这样用了!
function toFixed(number,fractionDigits){ var times = Math.pow(10, fractionDigits); var roundNum = Math.round(number * times) / times; return roundNum.toFixed(fractionDigits); }