JS浮點數(shù)運算結(jié)果不精確的Bug解決
前言
最近在做項目的時候,涉及到產(chǎn)品價格的計算,經(jīng)常會出現(xiàn)JS浮點數(shù)精度問題,這個問題,對于財務管理系統(tǒng)的開發(fā)者來說,是個非常嚴重的問題(涉及到錢相關的問題都是嚴重的問題),這里把相關的原因和問題的解決方案整理一下,也希望給各位提供一些參考。
一. 常見例子
// 加法 0.1 + 0.2 = 0.30000000000000004 0.1 + 0.7 = 0.7999999999999999 0.2 + 0.4 = 0.6000000000000001 // 減法 0.3 - 0.2 = 0.09999999999999998 1.5 - 1.2 = 0.30000000000000004 // 乘法 0.8 * 3 = 2.4000000000000004 19.9 * 100 = 1989.9999999999998 // 除法 0.3 / 0.1 = 2.9999999999999996 0.69 / 10 = 0.06899999999999999 // 比較 0.1 + 0.2 === 0.3 // false (0.3 - 0.2) === (0.2 - 0.1) // false
二. 導致原因
JavaScript 內(nèi)部只有一種數(shù)字類型Number,也就是說,JavaScript 語言的底層根本沒有整數(shù),所有數(shù)字都是以IEEE-754標準格式64位浮點數(shù)形式儲存,1與1.0是相同的。因為有些小數(shù)以二進制表示位數(shù)是無窮的。JavaScript會把超出53位之后的二進制舍棄,所以涉及小數(shù)的比較和運算要特別小心。
三. IEEE二進制浮點數(shù)算術標準(IEEE 754)
IEEE二進制浮點數(shù)算術標準(IEEE 754)是20世紀80年代以來最廣泛使用的浮點數(shù)運算標準,為許多CPU與浮點運算器所采用。這個標準定義了表示浮點數(shù)的格式(包括負零-0)與反常值(denormal number)),一些特殊數(shù)值(無窮(Inf)與非數(shù)值(NaN)),以及這些數(shù)值的“浮點數(shù)運算符”;它也指明了四種數(shù)值舍入規(guī)則和五種例外狀況(包括例外發(fā)生的時機與處理方式)。
四. 浮點數(shù)的存儲
JS的浮點數(shù)實現(xiàn)也是遵循IEEE 754標準,采用雙精度存儲(double precision),使用64位固定長度來表示,其中1位用來表示符號位,11位用來表示指數(shù),52位表示尾數(shù)。如下圖:
- 符號位(sign):第1位是正負數(shù)符號位,0代表正數(shù),1代表負數(shù)
- 指數(shù)位(Exponent):中間11位存儲指數(shù),用來表示次方數(shù)
- 尾數(shù)位(mantissa):最后的52位是尾數(shù),超出部分自動進一舍零
五. 浮點數(shù)的計算步驟(0.1+0.2)
【1】首先,十進制的0.1和0.2會轉(zhuǎn)換成二進制的,但是由于浮點數(shù)用二進制表示是無窮的
0.1——>0.0001 1001 1001 1001 ...(1001循環(huán))
0.2——>0.0011 0011 0011 0011 ...(0011循環(huán))
【2】IEEE754標準的64位雙精度浮點數(shù)的小數(shù)部分最多支持53位二進制,多余的二進制數(shù)字被截斷,所以兩者相加之后的二進制之和是
0.0100110011001100110011001100110011001100110011001101
【3】將截斷之后的二進制數(shù)字再轉(zhuǎn)換為十進制,就成了0.30000000000000004,所以在計算時產(chǎn)生了誤差
六. 解決辦法
【1】引用類庫
【2】思路一:在知道小數(shù)位個數(shù)的前提下,可以考慮通過將浮點數(shù)放大倍數(shù)到整型(最后再除以相應倍數(shù)),再進行運算操作,這樣就能得到正確的結(jié)果了
0.1 + 0.2 ——> (0.1 * 10 + 0.2 * 10) / 10 // 0.3
0.8 * 3 ——> ( 0.8 * 100 * 3) / 100 //2.4
【3】自定義一個轉(zhuǎn)換和處理函數(shù)
// f代表需要計算的表達式,digit代表小數(shù)位數(shù) Math.formatFloat = function (f, digit) { // Math.pow(指數(shù),冪指數(shù)) 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】加法函數(shù)
/** ** 加法函數(shù),用來得到精確的加法結(jié)果 ** 說明:javascript的加法結(jié)果會有誤差,在兩個浮點數(shù)相加的時候會比較明顯。這個函數(shù)返回較為精確的加法結(jié)果。 ** 調(diào)用:accAdd(arg1,arg2) ** 返回值:arg1加上arg2的精確結(jié)果 **/ 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方法,調(diào)用起來更加方便。 Number.prototype.add = function (arg) { return accAdd(arg, this); };
【5】減法函數(shù)
/** ** 減法函數(shù),用來得到精確的減法結(jié)果 ** 說明:javascript的減法結(jié)果會有誤差,在兩個浮點數(shù)相減的時候會比較明顯。這個函數(shù)返回較為精確的減法結(jié)果。 ** 調(diào)用:accSub(arg1,arg2) ** 返回值:arg1加上arg2的精確結(jié)果 **/ 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 //動態(tài)控制精度長度 n = (r1 >= r2) ? r1 : r2; return ((arg1 * m - arg2 * m) / m).toFixed(n); } // 給Number類型增加一個mul方法,調(diào)用起來更加方便。 Number.prototype.sub = function (arg) { return accMul(arg, this); };
【6】乘法函數(shù)
/** ** 乘法函數(shù),用來得到精確的乘法結(jié)果 ** 說明:javascript的乘法結(jié)果會有誤差,在兩個浮點數(shù)相乘的時候會比較明顯。這個函數(shù)返回較為精確的乘法結(jié)果。 ** 調(diào)用:accMul(arg1,arg2) ** 返回值:arg1乘以 arg2的精確結(jié)果 **/ 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方法,調(diào)用起來更加方便。 Number.prototype.mul = function (arg) { return accMul(arg, this); };
【7】除法函數(shù)
/** ** 除法函數(shù),用來得到精確的除法結(jié)果 ** 說明:javascript的除法結(jié)果會有誤差,在兩個浮點數(shù)相除的時候會比較明顯。這個函數(shù)返回較為精確的除法結(jié)果。 ** 調(diào)用:accDiv(arg1,arg2) ** 返回值:arg1除以arg2的精確結(jié)果 **/ 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方法,調(diào)用起來更加方便。 Number.prototype.div = function (arg) { return accDiv(this, arg); };
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學習或者工作具有一定的參考學習價值,謝謝大家對腳本之家的支持。
相關文章
javascript forEach通用循環(huán)遍歷方法
循環(huán)遍歷一個元素是開發(fā)中最常見的需求之一,那么讓我們來看一個由框架BASE2和Jquery的結(jié)合版本吧.2010-10-10ES6中Promise、async、await用法超詳細講解指南
async+await是ES6中引入的異步編程解決方案,旨在解決異步編程中的回調(diào)地獄問題,下面這篇文章主要給大家介紹了關于ES6中Promise、async、await用法超詳細講解的相關資料,需要的朋友可以參考下2024-08-08一個字符串中出現(xiàn)次數(shù)最多的字符 統(tǒng)計這個次數(shù)【實現(xiàn)代碼】
下面小編就為大家?guī)硪黄粋€字符串中出現(xiàn)次數(shù)最多的字符 統(tǒng)計這個次數(shù)【實現(xiàn)代碼】。小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考2016-04-04借助script進行Http跨域請求:JSONP實現(xiàn)原理及代碼
script元素的src屬性能設置URL并發(fā)起HTTP GET請求實現(xiàn)腳本操作HTTP可以跨域通信而不受限與同源策略,接下來為大家詳細介紹下Http跨域請求實現(xiàn),感興趣的你可以參考下哈2013-03-03