在 JavaScript 中整數和浮點數都屬於 Number
數據類型,全部數字都是以 64 位浮點數形式儲存,即使整數也是如此。 因此咱們在打印 1.00
這樣的浮點數的結果是 1
而非 1.00
。在一些特殊的數值表示中,例如金額,這樣看上去有點變扭,可是至少值是正確了。然而要命的是,當浮點數作數學運算的時候,你常常會發現一些問題,舉幾個例子:javascript
// 加法 ===================== // 0.1 + 0.2 = 0.30000000000000004 // 0.7 + 0.1 = 0.7999999999999999 // 0.2 + 0.4 = 0.6000000000000001 // 2.22 + 0.1 = 2.3200000000000003 // 減法 ===================== // 1.5 - 1.2 = 0.30000000000000004 // 0.3 - 0.2 = 0.09999999999999998 // 乘法 ===================== // 19.9 * 100 = 1989.9999999999998 // 19.9 * 10 * 10 = 1990 // 1306377.64 * 100 = 130637763.99999999 // 1306377.64 * 10 * 10 = 130637763.99999999 // 0.7 * 180 = 125.99999999999999 // 9.7 * 100 = 969.9999999999999 // 39.7 * 100 = 3970.0000000000005 // 除法 ===================== // 0.3 / 0.1 = 2.9999999999999996 // 0.69 / 10 = 0.06899999999999999
彷佛是難以想象。小學生都會算的題目,JavaScript 不會?咱們來看看其真正的緣由。css
JavaScript 裏的數字是採用 IEEE 754 標準的 64 位雙精度浮點數。該規範定義了浮點數的格式,對於64位的浮點數在內存中的表示,最高的1位是符號位,接着的11位是指數,剩下的52位爲有效數字,具體:前端
如圖:java
符號位決定了一個數的正負,指數部分決定了數值的大小,小數部分決定了數值的精度。 IEEE 754規定,有效數字第一位默認老是1,不保存在64位浮點數之中。也就是說,有效數字老是1.xx…xx的形式,其中xx..xx的部分保存在64位浮點數之中,最長可能爲52位。所以,JavaScript提供的有效數字最長爲53個二進制位(64位浮點的後52位+有效數字第一位的1)。git
好比在 JavaScript 中計算 0.1 + 0.2
時,到底發生了什麼呢?github
首先,十進制的0.1
和0.2
都會被轉換成二進制,但因爲浮點數用二進制表達時是無窮的,例如。編程
0.1 -> 0.0001100110011001...(無限) 0.2 -> 0.0011001100110011...(無限)
IEEE 754 標準的 64 位雙精度浮點數的小數部分最多支持 53 位二進制位,因此二者相加以後獲得二進制爲:後端
0.0100110011001100110011001100110011001100110011001100
因浮點數小數位的限制而截斷的二進制數字,再轉換爲十進制,就成了 0.30000000000000004
。因此在進行算術計算時會產生偏差。瀏覽器
在 Javascript 中,整數精度一樣存在問題,先來看看問題:安全
console.log(19571992547450991); //=> 19571992547450990 console.log(19571992547450991===19571992547450992); //=> true
一樣的緣由,在 JavaScript 中 Number
類型統一按浮點數處理,整數是按最大54位來算最大(253 - 1
,Number.MAX_SAFE_INTEGER
,9007199254740991
) 和最小(-(253 - 1)
,Number.MIN_SAFE_INTEGER
,-9007199254740991
) 安全整數範圍的。因此只要超過這個範圍,就會存在被捨去的精度問題。
固然這個問題並不僅是在 Javascript 中才會出現,幾乎全部的編程語言都採用了 IEEE-745 浮點數表示法,任何使用二進制浮點數的編程語言都會有這個問題,只不過在不少其餘語言中已經封裝好了方法來避免精度的問題,而 JavaScript 是一門弱類型的語言,從設計思想上就沒有對浮點數有個嚴格的數據類型,因此精度偏差的問題就顯得格外突出。
上面說了這麼多問題和緣由,這裏給出一些解決方案。
一般這種對精度要求高的計算都應該交給後端去計算和存儲,由於後端有成熟的庫來解決這種計算問題。前端也有幾個不錯的類庫:
Math.js 是專門爲 JavaScript 和 Node.js 提供的一個普遍的數學庫。它具備靈活的表達式解析器,支持符號計算,配有大量內置函數和常量,並提供集成解決方案來處理不一樣的數據類型
像數字,大數字(超出安全數的數字),複數,分數,單位和矩陣。 功能強大,易於使用。
GitHub:https://github.com/josdejong/mathjs
爲 JavaScript 提供十進制類型的任意精度數值。
官網:http://mikemcl.github.io/decimal.js/
GitHub:https://github.com/MikeMcl/decimal.js
官網:http://mikemcl.github.io/big.js
GitHub:https://github.com/MikeMcl/big.js/
這幾個類庫幫咱們解決不少這類問題,不過一般咱們前端作這類運算一般只用於表現層,應用並非不少。因此不少時候,一個函數能解決的問題不須要引用一個類庫來解決。
下面介紹各個更加簡單的解決方案。
對於整數,咱們能夠經過用String
類型的表示來取值或傳值,不然會喪失精度。
若是隻是格式化數字、金額、保留幾位小數等能夠查看這裏 http://www.css88.com/archives/7324
浮點數運算的解決方案有不少,這裏給出一種目前經常使用的解決方案, 在判斷浮點數運算結果前對計算結果進行精度縮小,由於在精度縮小的過程總會自動四捨五入。
toFixed() 方法使用定點表示法來格式化一個數,會對結果進行四捨五入。語法爲:
numObj.toFixed(digits)
參數 digits
表示小數點後數字的個數;介於 0 到 20 (包括)之間,實現環境可能支持更大範圍。若是忽略該參數,則默認爲 0。
返回一個數值的字符串表現形式,不使用指數記數法,而是在小數點後有 digits
位數字。該數值在必要時進行四捨五入,另外在必要時會用 0 來填充小數部分,以便小數部分有指定的位數。 若是數值大於 1e+21
,該方法會簡單調用 Number.prototype.toString()
並返回一個指數記數法格式的字符串。
特別注意:toFixed() 返回一個數值的字符串表現形式。
具體能夠查看 MDN中的說明,那麼咱們能夠這樣解決精度問題:
parseFloat((數學表達式).toFixed(digits)); // toFixed() 精度參數須在 0 與20 之間 // 運行 parseFloat((1.0 - 0.9).toFixed(10)) // 結果爲 0.1 parseFloat((0.3 / 0.1).toFixed(10)) // 結果爲 3 parseFloat((9.7 * 100).toFixed(10)) // 結果爲 970 parseFloat((2.22 + 0.1).toFixed(10)) // 結果爲 2.32
在老版本的IE瀏覽器(IE 6,7,8)中,toFixed()
方法返回值不必定準確。因此這個方法之前不多用。以致於網上搜索出來的結果大都是下面這些方法。
還有一些其餘的解決方案,簡單的說須要將浮點數轉換字符串,分隔成爲整數部分和小數部分,小數部分再轉換爲整數,計算結果後,再轉換爲浮點數。這過程有點複雜…,網上找一下:
/** ** 加法函數,用來獲得精確的加法結果 ** 說明: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); };
/** ** 減法函數,用來獲得精確的減法結果 ** 說明: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); };
/** ** 乘法函數,用來獲得精確的乘法結果 ** 說明: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); };
/** ** 除法函數,用來獲得精確的除法結果 ** 說明: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); };