如果你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); }