JS精度丟失問題

最近開發的項目新需求中,須要一個金額的輸入框,其輸入上限爲16位整數帶兩位小數的浮點類型,因爲前臺顯示的金額是字符串型,在傳遞給後臺時使用了parseFloat()方法,卻發如今測試校驗最大臨界值時,發生了四捨五入,最終致使校驗失敗,由此引起了我對JS浮點數表示和運算了深刻理解。bash

JS浮點數精度丟失的緣由

因爲計算機的二進制實現和位數限制,有些數沒法有限表示。就像一些無理數不能有限表示,如 圓周率 3.1415926...,1.3333... 等。JS 遵循 IEEE 754 規範,採用雙精度存儲(double precision),佔用 64 bit。測試

  • 1位用來表示符號位
  • 11位用來表示指數
  • 52位表示尾數

由於在計算機最底層,數值的運算和操做都是採用二進制實現的,因此計算機沒有辦法精確表示浮點數,而只能用二進制近似相等的去表示浮點數的小數部分。ui

0.1 >> 0.0001 1001 1001 1001…(1001無限循環)
0.2 >> 0.0011 0011 0011 0011…(0011無限循環)
複製代碼

當進行計算或其餘操做時時,四捨五入(逢1進,逢0舍)將會致使最終的運算結果存在誤差。spa

而大整數也存在一樣的問題,由於表示尾數的尾數只有52位,所以 JS 中能精準表示的最大整數是 Math.pow(2, 53),即十進制9007199254740992。3d

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循環的無限數值。code

在Java、C、C++中,均有對浮點數值的特殊處理,如Java的BigDecimal類型就是用來解決這一浮點數問題。cdn

常見的出錯場合

浮點數計算、比較:

0.1 + 0.2 != 0.3 // true
複製代碼

大整數計算、比較:

普通的整數計算比較不太容易出錯,除非計算範圍超出 Math.pow(2, 53)blog

9999999999999999 == 10000000000000001 // true
複製代碼

多位數字符數值轉換:

這種狀況在一些金額的計算中較容易出現,但也是最容易被忽視的一種,當用戶在輸入框中輸入一個位數較多的字符串(不單單包含大數值,小數點後位數過長也包含在這一案例中),並在前臺使用JS將其轉換爲數值,獲得的結果每每是四捨五入帶有誤差的值ci

parseFloat(0.9);    //0.9
parseFloat(9999999999999999.9)    //10000000000000000
parseInt("9999999999999999");    //10000000000000000
parseFloat(9.999999999999999);   //10
複製代碼

toFixed不會四捨五入:

var num = 1.335;
num.toFixed(2);   //1.33
複製代碼

解決方案

浮點數計算、比較:

一般解決這一問題,採用的都是將浮點部分轉換成整數後進行計算開發

//浮點數轉換爲整數
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的修復:

function toFixed(num, s) {
    var times = Math.pow(10, s)
    var des = num * times + 0.5
    des = parseInt(des, 10) / times
    return des + ''
}
複製代碼
相關文章
相關標籤/搜索