【JS】JS中2個數(浮點數)計算不準確問題
阿新 • • 發佈:2020-08-28
專案中遇到的問題
1000 + 217.4 = 1217.1399999999999
一. 常見例子
//加法 console.log(0.1 + 0.2); // 0.30000000000000004 console.log(0.1 + 0.7); //0.7999999999999999 console.log(0.2 + 0.4); //0.6000000000000001 // 減法 console.log(0.3 - 0.2); //0.09999999999999998 console.log(1.5 - 1.2); //0.30000000000000004 //乘法 console.log(0.8 * 3); //2.4000000000000004 console.log(19.9 * 100); //1989.9999999999998 //除法 console.log(0.3 / 0.1); //2.9999999999999996 console.log(0.69 / 10); //0.06899999999999999 //比較 console.log(0.1 + 0.2 == 0.3); //false console.log((0.3 - 0.2) == (0.2- 0.1)); //false
二. 導致原因
JavaScript 內部只有一種數字型別Number,也就是說,JavaScript 語言的底層根本沒有整數,所有數字都是以IEEE-754標準格式64位浮點數形式儲存,
1
與1.0
是相同的。因為有些小數以二進位制表示位數是無窮的。JavaScript會把超出53位之後的二進位制捨棄,所以涉及小數的比較和運算要特別小心。
三. IEEE二進位制浮點數算術標準(IEEE 754)
IEEE二進位制浮點數算術標準(IEEE 754)是20世紀80年代以來最廣泛使用的浮點數運算標準,為許多CPU與浮點運算器所採用。這個標準定義了表示浮點數的格式(包括負零-0)與反常值(denormal number)),一些特殊數值(無窮(Inf)與非數值(NaN)),以及這些數值的“浮點數運算子”;它也指明瞭四種數值舍入規則和五種例外狀況(包括例外發生的時機與處理方式)。
四. 浮點數的儲存
JS的浮點數實現也是遵循IEEE 754標準,採用雙精度儲存(double precision),使用64位固定長度來表示,其中1位用來表示符號位,11位用來表示指數,52位表示尾數。如下圖:
- 符號位(sign):第1位是正負數符號位,0代表正數,1代表負數
- 指數位(Exponent):中間11位儲存指數,用來表示次方數
- 尾數位(mantissa):最後的52位是尾數,超出部分自動進一舍零
五. 浮點數的計算步驟(0.1+0.2)
【1】首先,十進位制的0.1和0.2會轉換成二進位制的,但是由於浮點數用二進位制表示是無窮的
0.1——>0.0001 1001 1001 1001 ...(1001迴圈)
0.2——>0.0011 0011 0011 0011 ...(0011迴圈)
【2】IEEE754標準的64位雙精度浮點數的小數部分最多支援53位二進位制,多餘的二進位制數字被截斷,所以兩者相加之後的二進位制之和是
0.0100110011001100110011001100110011001100110011001101
【3】將截斷之後的二進位制數字再轉換為十進位制,就成了0.30000000000000004,所以在計算時產生了誤差
六. 解決辦法
【1】引用類庫
【2】思路一:在知道小數位個數的前提下,可以考慮通過將浮點數放大倍數到整型(最後再除以相應倍數),再進行運算操作,這樣就能得到正確的結果了
0.1 + 0.2 ——> (0.1 * 10 + 0.2 * 10) / 10 // 0.3 0.8 * 3 ——> ( 0.85 * 100 * 3) / 100 //2.4
【3】自定義一個轉換和處理函式
// f代表需要計算的表示式,digit代表小數位數 Math.formatFloat = function (f, digit) { // Math.pow(指數,冪指數) var m = Math.pow(10, digit); // Math.round() 四捨五入 return Math.round(f * m, 10) / m; } console.log(Math.formatFloat(0.3 * 8, 1)); // 2.4 console.log(Math.formatFloat(0.35 * 8, 2)); // 2.8
【4】加法函式
/** ** 加法函式,用來得到精確的加法結果 ** 說明:javascript的加法結果會有誤差,在兩個浮點數相加的時候會比較明顯。這個函式返回較為精確的加法結果。 ** 呼叫:accAdd(arg1,arg2) ** 返回值:arg1加上arg2的精確結果 **/ function accAdd(arg1, arg2) { var r1, r2, m, c; try { r1 = arg1.toString().split(".")[1].length; } catch (e) { r1 = 0; } try { r2 = arg2.toString().split(".")[1].length; } catch (e) { r2 = 0; } c = Math.abs(r1 - r2); m = Math.pow(10, Math.max(r1, r2)); if (c > 0) { var cm = Math.pow(10, c); if (r1 > r2) { arg1 = Number(arg1.toString().replace(".", "")); arg2 = Number(arg2.toString().replace(".", "")) * cm; } else { arg1 = Number(arg1.toString().replace(".", "")) * cm; arg2 = Number(arg2.toString().replace(".", "")); } } else { arg1 = Number(arg1.toString().replace(".", "")); arg2 = Number(arg2.toString().replace(".", "")); } return (arg1 + arg2) / m; } //給Number型別增加一個add方法,呼叫起來更加方便。 Number.prototype.add = function (arg) { return accAdd(arg, this); };
【5】減法函式
/** ** 減法函式,用來得到精確的減法結果 ** 說明:javascript的減法結果會有誤差,在兩個浮點數相減的時候會比較明顯。這個函式返回較為精確的減法結果。 ** 呼叫:accSub(arg1,arg2) ** 返回值:arg1加上arg2的精確結果 **/ function accSub(arg1, arg2) { var r1, r2, m, n; try { r1 = arg1.toString().split(".")[1].length; } catch (e) { r1 = 0; } try { r2 = arg2.toString().split(".")[1].length; } catch (e) { r2 = 0; } m = Math.pow(10, Math.max(r1, r2)); //last modify by deeka //動態控制精度長度 n = (r1 >= r2) ? r1 : r2; return ((arg1 * m - arg2 * m) / m).toFixed(n); } // 給Number型別增加一個mul方法,呼叫起來更加方便。 Number.prototype.sub = function (arg) { return accMul(arg, this); };
【6】乘法函式
/** ** 乘法函式,用來得到精確的乘法結果 ** 說明:javascript的乘法結果會有誤差,在兩個浮點數相乘的時候會比較明顯。這個函式返回較為精確的乘法結果。 ** 呼叫:accMul(arg1,arg2) ** 返回值:arg1乘以 arg2的精確結果 **/ function accMul(arg1, arg2) { var m = 0, s1 = arg1.toString(), s2 = arg2.toString(); try { m += s1.split(".")[1].length; } catch (e) {} try { m += s2.split(".")[1].length; } catch (e) {} return Number(s1.replace(".", "")) * Number(s2.replace(".", "")) / Math.pow(10, m); } // 給Number型別增加一個mul方法,呼叫起來更加方便。 Number.prototype.mul = function (arg) { return accMul(arg, this); };
【7】除法函式
/** ** 除法函式,用來得到精確的除法結果 ** 說明:javascript的除法結果會有誤差,在兩個浮點數相除的時候會比較明顯。這個函式返回較為精確的除法結果。 ** 呼叫:accDiv(arg1,arg2) ** 返回值:arg1除以arg2的精確結果 **/ function accDiv(arg1, arg2) { var t1 = 0, t2 = 0, r1, r2; try { t1 = arg1.toString().split(".")[1].length; } catch (e) {} try { t2 = arg2.toString().split(".")[1].length; } catch (e) {} with(Math) { r1 = Number(arg1.toString().replace(".", "")); r2 = Number(arg2.toString().replace(".", "")); return (r1 / r2) * pow(10, t2 - t1); } } //給Number型別增加一個div方法,呼叫起來更加方便。 Number.prototype.div = function (arg) { return accDiv(this, arg); };