在開發中,要進行計算,你可能會遇到小數運算,運氣好的話,你的測試測不到精度問題,但其實這是很嚴重的,如下兩個典型例子先感覺如下java
0.1 + 0.2 = 0.30000000000000004
35.41 * 100 = 3540.9999999999995
複製代碼
是否是出乎你的意料?安全
寫這篇文章的緣由是網上找了些資料,要不就是介紹不全的,要不就是存在錯誤的(可能你們沒發現),要不就是方案還有待增強的。因而我決定本身整理出一份較爲全面而不誤導別人的文章出來(文章方案對網上大部分資料存在的缺陷進行彌補加強),若是您發現不足,請告訴我,虛心請教。bash
如下咱們瞭解緣由以及尋找解決方案。ide
在計算機中存儲的信息都是二進制來表示,咱們都知道,js中數字類型只有Number
,不像其餘語言如java
有int
、double
類型等。它的實現遵循 IEEE 754 標準,使用64位固定長度來表示,也就是標準的 double 雙精度浮點數。函數
其中這64位數又分爲三部分(從左往右看):測試
以例子0.1 + 0.3
展開說明:ui
0.1
-> 0.0001100110011001100110011001100110011001100110011010
; 0.2
-> 0.0011001100110011001100110011001100110011001100110011
0.0100110011001100110011001100110011001100110011001101
,轉化爲十進制就是0.30000000000000004
因而就這樣,獲得了一個出乎你預料的結果了。並非全部小數計算都會這樣,是一些小數轉化位二進制時出現超出52位數時,就可能會出現這種意外結果。spa
上面咱們知道了出現意外結果是由於小數轉化爲二進制時發生問題,那整數呢?很天然,整數也是有最大值安全值的,就是2的53次方,爲9007199254740992。超出這個數值的計算一樣也是帶來精度問題。可是咱們通常使用是不會超出這個值的(若是你的需求也要考慮超出這個數值,抱歉,我無能爲力)code
若是你的需求能夠無視上面整數最大安全值的弊端,那麼接下來的解決方案纔是適合你的。ip
解放方案:把小數運算中的小數,升級轉化爲整數(乘以10的n次冪),在進行運算,將最後結果再降級(除以10的n次冪)
上面的描述僅僅是一個思路,一個轉化思路。怎麼將小數轉化爲整數,這就講究了,不能真的進行乘法計算,乘以10的n次冪,還記得前言裏的第二個例子嗎,這種轉化整數的方式自己就是一個小數運算,因此仍是會出現問題。
咱們要用字符串替換的方式來實現這個「升級」:
原值:1.23
轉化過程:
1. 化爲字符串 '1.23'
2. '1.23'.replace('.', ''),得'123',至關於乘以10的2次冪
複製代碼
結合實際例子來了解大概的一個狀況,例子1.1 + 1.22
1. 分別對1.1和1.22進行字符串替換,變成'110'和'122',至關於乘以10的2次冪
2. 對替換結果轉化回數字類型而後再進行加法運算:110 + 122 = 232
3. 對232除以10的2次冪,得2.32
複製代碼
以上就是一個轉化和運算過程。
基於上述基本思想,我對加減乘除進行一個方法封裝,方便你們進行小數運算。
/** * 帶有小數的加法/減法運算 * 減法實際上可當作加法,因此若是要作減法,只需第二個參數即被減數傳負值便可 * @param {Number} arg1 - 加數/減數 * @param {Number} arg2 - 加數/被減數 */
function addFloat(arg1, arg2) {
let m = 0; // 記錄兩個加數中最長的小數位長度
let arg1Str = arg1 + '';
let arg2Str = arg2 + '';
const arg1StrFloat = arg1Str.split('.')[1];
const arg2StrFloat = arg2Str.split('.')[1];
arg1StrFloat && (m = arg1StrFloat.length);
arg2StrFloat && (m = m > arg2StrFloat.length ? m : arg2StrFloat.length);
arg1Str = arg1.toFixed(m); // 主要是爲了補零
arg2Str = arg2.toFixed(m);
const transferResult = +(arg1Str.replace('.', '')) + +(arg2Str.replace('.', ''));
return transferResult / Math.pow(10, m);
};
/** * 帶有小數的乘法運算 * @param {Number} arg1 - 因數 * @param {Number} arg2 - 因數 */
function multiplyFloat(arg1, arg2) {
let m = 0;
const arg1Str = arg1 + '';
const arg2Str = arg2 + '';
const arg1StrFloat = arg1Str.split('.')[1];
const arg2StrFloat = arg2Str.split('.')[1];
arg1StrFloat && (m += arg1StrFloat.length);
arg2StrFloat && (m += arg2StrFloat.length);
const transferResult = +(arg1Str.replace('.', '')) * +(arg2Str.replace('.', ''));
return transferResult / Math.pow(10, m);;
};
/** * 有小數的除法運算 * @param {Number} arg1 - 除數 * @param {Number} arg2 - 被除數 */
function divideFloat(arg1, arg2) {
const arg1Str = arg1 + '';
const arg2Str = arg2 + '';
const arg1StrFloat = arg1Str.split('.')[1] || '';
const arg2StrFloat = arg2Str.split('.')[1] || '';
const m = arg2StrFloat.length - arg1StrFloat.length;
const transferResult = +(arg1Str.replace('.', '')) / +(arg2Str.replace('.', ''));
return transferResult * Math.pow(10, m);;
};
複製代碼
任何一個方案都不能十全十美的,多多少少會有一些限制,畢竟需求是多種多樣的。我寫文章的習慣就是得告知別人缺陷,而不能忽悠別人。知道本身寫的東西的利,也得知道本身的弊。
該方案會有幾個小缺陷:
9007199254740992
toFixed
方法,該方法的參數num有個限制:當 num 過小或太大時拋出異常 RangeError。0 ~ 20 之間的值不會引起該異常。以上着兩個小缺陷其實在咱們正常開發中,通常不會觸及到,由於這樣的數字和小數位實在太長了,咱們通常需求不會要求進行這麼大的運算以及小數點保留位。
這裏順着這個主題,能夠順帶講一下在js中進行四捨五入或進行小數位保留的狀況。
不少人會想到,用tofixed進行四捨五入,實際上,tofixed函數對於四捨五入的規則與數學中的規則不一樣,使用的是銀行家舍入規則:實際上是一種四捨六入五取偶(又稱四捨六入五留雙)法。表現爲:
四捨六入五考慮,五後非零就進一,五後爲零看奇偶,五前爲偶應捨去,五前爲奇要進一。
很顯然,這並非咱們想要的結果。可是Math.round
方法,就是咱們所熟知的四捨五入規則,咱們能夠利用該方法擴展到小數位的四捨五入。
網上不少資料都有介紹這種方式:
對小數乘以10的n次冪,再用Math.round取整,再除以10的n次冪,就能獲得進過四捨五入後的指定小數位了。
通過上文個人介紹,只要對小數進行數學運算,都有可能出現精度不許確的問題。因此最終的一步正如網上這麼多資料說的那樣作法,可是其中的乘法和除法,請用文中封裝好的方法來進行,而不是直接進行小數的數學運算。
這裏我封裝一個四捨五入的方法
function roundFloat (value, decimal = 2) {
const n = Math.pow(10, decimal);
return divideFloat(Math.round(multiplyFloat(value, n)), n).toFixed(decimal);
}
複製代碼
可能有人會疑問,我這裏爲何還要用toFixed
,不是說這個不許的嗎?
其實我這裏並無用toFixed
作實際性上的四捨五入,真正作了四捨五入的工做在調用toFixed
前就已經完成了,最後還用toFixed
只是作潤色做用,例如1.1
你要保留小數點後兩位的話,理應顯示成1.10
,可是js中數字類型顯示出來的話,是不會有後面的不起做用的0,所以若是你想顯示出指定位數,不足補零的話,就得用toFixed
轉化位字符串了。
所以上面的方法的返回結果是一個字符串類型,這你們要注意了,若是你沒有這方面的需求,可自行拿掉後面的toFixed
調用。
未經容許,請勿私自轉載