從0.1+0.2=0.30000000000000004再看JS中的Number類型

寫在前面

今天在看《JavaScript高級程序設計》的時候,注意到書中特地提到了0.1+0.2=0.30000000000000004這樣一個浮點數計算錯誤的問題,以爲頗有意思。平時在工做中對於浮點數了解地並很少,正好最近小組同窗也遇到了這個問題,準備來總結下這個看似簡單的Number基礎類型,其實並不簡單。這篇博客意在從這個奇怪的計算結果去學習總結浮點數的相關知識。javascript

兩個既定的事實

  1. 在JS中可否表示的數字的絕對值範圍是5e-324 ~ 1.7976931348623157e+308,這一點能夠經過Number.MAX_VALUENumber.MIN_VALUE來獲得證明
  2. 在JS中可以表示的最大安全整數的範圍是:-9007199254740991 ~ 9007199254740991,這一點能夠經過Number.MIN_SAFE_INTEGERNumber.MAX_SAFE_INTEGER來求證

兩個存在的問題

  1. 在四則運算中存在精度丟失的問題,好比: 01 + 0.2 //0.30000000000000004
  2. 超過最大安全整數的運算是不安全的,好比:9007199254740991 + 2 // 9007199254740992

Why?

想要解釋清楚上述的兩個事實和問題,須要先知道小數在計算機中是如何存儲的:前端

知識點!!!

  1. 把這個浮點數轉成對應的二進制數,並用科學計數法表示
  2. 把這個數值經過IEEE 754標準表示成真正會在計算機中存儲的值

咱們知道,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:廣告一波,網易考拉前端招人啦~~~有興趣的戳我投簡歷

image
相關文章
相關標籤/搜索