今天在看《JavaScript高級程序設計》的時候,注意到書中特地提到了0.1+0.2=0.30000000000000004
這樣一個浮點數計算錯誤的問題,以爲頗有意思。平時在工做中對於浮點數了解地並很少,正好最近小組同窗也遇到了這個問題,準備來總結下這個看似簡單的Number基礎類型,其實並不簡單。這篇博客意在從這個奇怪的計算結果去學習總結浮點數的相關知識。javascript
Number.MAX_VALUE
和Number.MIN_VALUE
來獲得證明Number.MIN_SAFE_INTEGER
和Number.MAX_SAFE_INTEGER
來求證01 + 0.2 //0.30000000000000004
9007199254740991 + 2 // 9007199254740992
想要解釋清楚上述的兩個事實和問題,須要先知道小數在計算機中是如何存儲的:前端
咱們知道,JS中的Number類型使用的是雙精度浮點型,也就是其餘語言中的double類型。而雙精度浮點數使用64 bit來進行存儲,結構圖以下:java
也就是說一個Number類型的數字在內存中會被表示成:s x m x 2^e
這樣的格式。git
在ES規範中規定e的範圍在-1074 ~ 971,而m最大能表示的最大數是52個1,最小能表示的是1,這裏須要注意:github
二進制的第一位有效數字一定是1,所以這個1不會被存儲,能夠節省一個存儲位,所以尾數部分能夠存儲的範圍是1 ~ 2^(52+1)安全
也就是說Number能表示的最大數字絕對值範圍是 2^-1074 ~ 2^(53+971)bash
前面提到,計算機中存儲小數是先轉換成二進制進行存儲的,咱們來看一下0.1和0.2轉換成二進制的結果:微信
(0.1)10 => (00011001100110011001(1001)...)2
(0.2)10 => (00110011001100110011(0011)...)2
複製代碼
能夠發現,0.1和0.2轉成二進制以後都是一個無限循環的數,前面提到尾數位只能存儲最多53位有效數字,這時候就必須來進行四捨五入了,而這個取捨的規則就是在IEEE 754中定義的,0.1最終能被存儲的有效數字是markdown
0001(1001)(1001)(1001)(1001)(1001)(1001)(1001)(1001)(1001)(1001)(1001)(1001)101
+
(0011)(0011)(0011)(0011)(0011)(0011)(0011)(0011)(0011)(0011)(0011)(0011)(0011)01
=
0100(1100)(1100)(1100)(1100)(1100)(1100)(1100)(1100)(1100)(1100)(1100)(1100)111
複製代碼
這裏注意,53位的存儲位指的是能存53位有效數字,所以前置的0不算,要日後再取到53位有效數字爲止。函數
最終的這個二進制數轉成十進制就是0.30000000000000004(不信的話能夠找一個在線進制轉換工具試一下。
到此,這個精度丟失的問題已經解釋清楚了,用一句話來歸納就是,計算機中用二進制來存儲小數,而大部分小數轉成二進制以後都是無限循環的值,所以存在取捨問題,也就是精度丟失。
這裏直接推薦一篇文章,關於這個問題講的很是清楚(文中有一處錯誤,會在下面指出。
若是懶得看英文的話,能夠看個人總結:
最大安全整數9007199254740991對應的二進制數如圖:
53位有效數字都存儲滿了以後,想要表示更大的數字,就只能往指數數加一位,這時候尾數由於沒有多餘的存儲空間,所以只能補0。
如圖全部,在指數位爲53的狀況下,最後一位尾數位爲0的數字能夠被精確表示,而最後一位尾數爲爲1的數字都不能被精確表示。也就是能夠被精確表示和不能被精確表示的比例是1:1
。
同理,當指數爲54的時候,只有最後兩位尾數爲00的能夠被精確表示,也就是能夠被精確表示和不能被精確表示的比例是1:3
,當有效位數達到x(x>53)
的時候,能夠被精確表示和不能被精確表示的比例將是1 : 2^(x-53) - 1
。
能夠預見的是,在指數愈來愈高的時候,這個指數會成指數增加,所以在Number.MAX_SAFE_INTEGER ~ Number.MAX_VALUE之間能夠被精確表示的整數能夠說是百裏挑一。
我發現這篇文章中的一個錯誤,文章中指出9007199254740998這個數字不能被精確表示,其實是能夠的,在指數位是53的狀況下,偶數能夠被精確表示,奇數不能被精確表示,不能被精確表示的最小偶數應該是當指數位爲54,而且最後兩位尾數爲0的時候。
之因此會有最大安全整數這個概念,本質上仍是由於數字類型在計算機中的存儲結構。在尾數位不夠補零以後,只要是多餘的尾數爲1所對應的整數都不能被精確表示。
能夠發現,不論是浮點數計算的計算結果錯誤和大整數的計算結果錯誤,最終均可以歸結到JS的精度只有53位(尾數只能存儲53位的有效數字)。那麼咱們在平常工做中碰到這兩個問題該如何解決呢?
大而全的解決方案就是使用mathjs,看一下mathjs的輸出:
math.config({ number: 'BigNumber', precision: 64 }); console.log(math.format(math.eval('0.1 + 0.2'))); // '0.3' console.log(math.format(math.eval('0.23 * 0.34 * 0.92'))); // '0.071944' console.log(math.format(math.eval('9007199254740991 + 2'))); // '9.007199254740993e+15' 複製代碼
其實平時在遇到整型溢出的狀況是很是少的,大部分場景是浮點數的計算,若是不想由於一些簡單的計算引入mathjs的話,也能夠本身來實現運算函數(須要考慮數字是否越界和當數字被表示成科學計數法的場景),若是懶得本身實現的話,可使用這個1k都不到的number-precision,這個工具庫API簡潔不少,已經能夠解決浮點數的計算問題了(看了代碼,對於超出Number.MAX_SAFE_INTEGER的數字的處理方式是拋出warning)。
ps:廣告一波,網易考拉前端招人啦~~~有興趣的戳我投簡歷