0.1+0.2 !== 0.3?
眾所周知,JavaScript在計算某些浮點數的運算時會出現精度的丟失,比如你在控制檯輸入 0.1+0.2
,得到的結果是 0.30000000000000004
而不是 0.3
,原因是什麼?
世界上有兩種人,懂二進位制和不懂二進位制的人
我們知道,計算機裡所有的資料最終都是以二進位制儲存的,當然數字也一樣。所以當計算機計算 0.1+0.2
的時候,實際上計算的是這兩個數字在計算機裡所儲存的二進位制,那麼 0.1
在JavaScript裡儲存的二進位制到底是多少? 我們先根據十進位制轉二進位制的方法,把 0.1
轉化為二進位制是: 0.0001100110011001100...
(1100迴圈),然後把 0.2
轉化為二進位制是: 0.00110011001100...
(1100迴圈)。 我們發現,它們都是無限迴圈的二進位制。顯然,計算機當然不會用自己“無限的空間”去儲存這些無限迴圈的二進位制數字。那對於這類資料該怎麼辦?
JavaScript如何儲存無限迴圈的二進位制小數?
不同的語言有不同的儲存標準,這裡我們暫且只討論JavaScript的儲存標準。JavaScript中所用的數字包括整數和小數,都只有一種型別就是 Number
,它的實現遵循IEEE 754標準,使用64位固定長度來表示,也就是標準的double雙精度浮點數(相關的還有float 32位單精度),具體的雙精度浮點數的儲存方式這裡不再贅述(可以看後面章節的詳細描述),我們只需要知道,在二進位制科學表示法中,雙精度浮點的小數部分最多隻能保留52位(比如 1.xxx...*2^n
,這裡 x
最多保留52位),再回過頭看 0.1
的二進位制表示:
0.00011001100110011001100110011001100110011001100110011001 10011... 複製程式碼
空格前為第53個有效數字,如何捨去後面的數字,這裡遵從“0舍1入”,那麼捨去之後實際上就是:
0.00011001100110011001100110011001100110011001100110011010 複製程式碼
同理我們得到 0.2
的捨去之後實際儲存的二進位制為:
0.0011001100110011001100110011001100110011001100110011010 複製程式碼
二者相加:
0.00011001100110011001100110011001100110011001100110011010 + 0.0011001100110011001100110011001100110011001100110011010 = 0.0100110011001100110011001100110011001100110011001100111 複製程式碼
我們把結果根據公式或者工具轉為十進位制:

可以看到結果正好為: 0.30000000000000004
。
注:大多數語言中的小數預設都是遵循 IEEE 754 的 float 浮點數,包括 Java、Ruby、Python,本文中的浮點數問題同樣存在。
浮點數是如何儲存的

在計算機中,浮點表示法,分為三大部分:
- 第一部分用來儲存符號位(sign),用來區分正負數,0表示正數
- 第二部分用來儲存指數(exponent)
- 第三部分用來儲存小數(fraction)
雙精度浮點數一共佔據64位:
- 符號位(sign)佔用1位
- 指數位(exponent)佔用11位
- 小數位(fraction)佔用52位
這裡的符號位、指數位、小數位跟二進位制是如何聯絡在一起呢? 我們以 78.735
為例

1.001110011*2^6
我們稱為二進位制的科學記數法,這個實數由一個整數或定點數(即尾數)乘以某個基數(計算機中通常是2)的整數次冪得到,這就叫
浮點數 。 我們對號入座,先把指數部分
6
轉化為二進位制是
110
,最終為:
0(sign) 00000000110(exponent) 00111001 10000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 複製程式碼
(這是錯誤的,具體為什麼錯,繼續看下文)
我們再根據雙精度規範,來看看上文提到的 0.1
到底是如何儲存的,我們已知它的二進位制是:
0.00011001100110011001100110011001100110011001100110011001 10011... 複製程式碼
轉化為科學表示法就是:
1.1001100110011001100110011001100110011001100110011001*2^-2 複製程式碼
也就是說 0.1
的:
0 1001100110011001100110011001100110011001100110011001 -2
到這裡我就懵逼了, -2
怎麼轉為二進位制呢,雖然雙精度浮點規範規定了一個符號位,但是這個符號位表示的是整個資料的正負,而非指數的正負,難道還要保留一位專門儲存指數的正負嗎?答案是否定的,為了減少不必要的麻煩,IEEE規定了一個偏移量,這個偏移量是幹嘛用的呢,就是對於指數部分,每次都加這個偏移量進行儲存,這樣即使指數是負數,那麼加上這個偏移量也變為正數啦。為了使所有的負指數加上這個偏移量都能夠變為正數,這個偏移量的設定也是有規律的。 以double雙精度為例,我們知道它的指數部分是二進位制的11位,那麼能夠表示的資料範圍就是 0~2047
,IEEE規定一個大概在中間數的位置 1023
為雙精度的偏移量。
- 當指數位不全是0也不全是1時(規格化的數值),IEEE規定,階碼計算公式為
e-Bias
。 此時e最小值是1,則1-1023= -1022
,e最大值是2046
,則2046-1023=1023
,可以看到,這種情況下取值範圍是-1022~1013
。 - 當指數位全部是0的時候(非規格化的數值),IEEE規定,階碼的計算公式為
1-Bias
,即1-1023= -1022
。 - 當指數位全部是1的時候(特殊值),IEEE規定這個浮點數可用來表示3個特殊值,分別是正無窮,負無窮,
NaN(not a number)
。 具體的,小數位不為0的時候表示NaN;小數位為0時,當符號位s=0時表示正無窮,s=1時候表示負無窮。
這個時候我們再看 78.735
的指數部分如何儲存,需要 6+1023
就是 1029
,轉化為二進位制就是: 10000000101
,所以 78.735
正確儲存方式為:
0 10000000101 00111001 10000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 複製程式碼
同理,你是否也知道 0.1
的雙精度的浮點儲存形式了呢?
浮點數值的範圍
如果你認真讀到了這裡,想必你應該能推算出JavaScript的所能表示的數值範圍了吧。 e的最大值是1023。 1.111..(52位)..11*2^1023
轉為普通二進位制就是:
1 111..(52位)..11 000..(1023-52就是971位)..00 複製程式碼
把二進位制轉為十進位制就是:

Number.MAX_VALUE
的值一致,都是
1.7976931348623157e+308
。 但實際上這個值還不算最大,比如我們在此數值基礎上繼續加一些數,發現並沒有返回
Infinity
。

Number.MAX_VALUE
和
Infinity
之間還存在一些數,根據IEEE規範我們可以得知,正無窮當且是指數部分全為1(指數部分的最大值
Math.pow(2,11)-1-1023 == 1024
),小數部分為0的時候,就是:
1.000...*2^1024 複製程式碼
所以 Math.pow(2,1024)
就是正無窮,那麼其實JavaScript所能儲存的最大數字是 Math.pow(2,1024)-1
。 但是 Number.MAX_VALUE
和 Math.pow(2,1024)
之間的資料我們無法正常表示出來,精度會丟失。 同理也可推算最小數。
JavaScript的安全最大整數
所謂安全範圍,就是我們在這個範圍內計算不會出現精度的丟失。 根據雙精度的定義,可以得知,最大的安全整數:
1.11..(52位)*2^52 複製程式碼
轉為十進位制就是 Math.pow(2,53)-1
,即 9007199254740991
。
在JavaScript中,有 Number.MAX_SAFE_INTEGER
來表示最大安全整數
