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

考拉海購前端團隊發表於2018-01-30

寫在前面

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

兩個既定的事實

  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來進行儲存,結構圖如下:

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

也就是說一個Number型別的數字在記憶體中會被表示成:s x m x 2^e這樣的格式。

ES規範中規定e的範圍在-1074 ~ 971,而m最大能表示的最大數是52個1,最小能表示的是1,這裡需要注意:

知識點!!!

二進位制的第一位有效數字必定是1,因此這個1不會被儲存,可以節省一個儲存位,因此尾數部分可以儲存的範圍是1 ~ 2^(52+1)

也就是說Number能表示的最大數字絕對值範圍是 2^-1074 ~ 2^(53+971)

精度丟失

前面提到,計算機中儲存小數是先轉換成二進位制進行儲存的,我們來看一下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最終能被儲存的有效數字是


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對應的二進位制數如圖: 從0.1+0.2=0.30000000000000004再看JS中的Number型別

53位有效數字都儲存滿了之後,想要表示更大的數字,就只能往指數數加一位,這時候尾數因為沒有多餘的儲存空間,因此只能補0。

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

如圖所有,在指數位為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

相關文章