前言:
在看了 JavaScript 浮點數陷阱及解法 和 探尋 JavaScript 精度問題 後,發現沒有具體詳細的推導0.1+0.2=0.30000000000000004
的過程,因此我寫了此文補充下css
正文:html
console.log(0.1+0.2) //0.30000000000000004
複製代碼
將 0.1 轉爲二進制:nginx
沒有整數部分
小數部分爲 0.1,乘 2 取整,直至沒有小數:
0.1 * 2 = 0.2 ...0
0.2 * 2 = 0.4 ...0
0.4 * 2 = 0.8 ...0
0.8 * 2 = 1.6 ...1
0.6 * 2 = 1.2 ...1
//開始循環
0.2 * 2 = 0.4 ...0
。。。
。。。
複製代碼
0.1 的二進制爲:0.0 0011 0011 0011 無限循環0011
git
採用科學計數法,表示 0.1 的二進制:github
//0.00011001100110011001100110011001100110011001100110011 無限循環0011
//因爲是二進制,因此 E 表示將前面的數字乘以 2 的 n 次冪
//注意:n 是十進制的數字,後文須要
2^(-4) * (1.1001100110011循環0011)
(-1)^0 * 2^(-4) * (1.1 0011 0011 0011 循環0011)
複製代碼
因爲 JavaScript 採用雙精度浮點數(Double)存儲number,因此它是用 64 位的二進制來存儲 number 的web
十進制與 Double 的相互轉換公式以下:segmentfault
V:表示十進制的結果
SEM:表示雙精度浮點數的結果(就是 S 拼 E 拼 M,不是相加)markdown
2^(-4) * (1.1001100110011循環0011)
套用此公式右邊,得:app
(-1)^0 * 2^(-4) * (1.1 0011 0011 0011 循環0011)
複製代碼
因此,工具
S = 0 //二進制
E = 1019 //十進制
M = 1001100110011循環0011 //二進制
複製代碼
雙精度浮點數 存儲結構以下:
由圖可知:
①
S 表示符號位,佔 1 位
E 表示指數位,佔 11 位
M 小數位,佔 52 位(若是第 53 位爲 1,須要進位!)
②
//二進制
S = 0 知足條件
//十進制
E = 1019 不知足條件,須要轉爲 11 位的二進制
//二進制
M = 1001100110011循環0011 不知足條件,須要轉爲 52 位的二進制
複製代碼
① 將 1019 轉爲 11 位的二進制
//1019
1111111011 ,共 10 位,但 E 要 11 位,因此要在首部補 0
E = 01111111011
複製代碼
在線轉換工具:在線轉換工具(BigNumber時不許確)
② 將1001100110011循環0011
轉爲 52 位的二進制
//1 0011 0011 0011 循環0011 第53位
1 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011
第 53 位爲 1,要進位,同時捨去第53位及其日後的
M = 1001100110011001100110011001100110011001100110011010 //共 52 位
複製代碼
綜上:
S = 0
E = 01111111011
M = 1001100110011001100110011001100110011001100110011010
複製代碼
拼接 SEM 獲得 64 位雙精度浮點數:
S E M
0 01111111011 1001100110011001100110011001100110011001100110011010
//合併獲得 64 位雙精度浮點數
0011111110111001100110011001100110011001100110011001100110011010
複製代碼
故 0.1 在 JavaScript 中存儲的真實結構爲:0011111110111001100110011001100110011001100110011001100110011010
經過 Double相互轉換十進制(它是我找獲得的有效位數最多的網站) 得:
1.00000000000000005551115123126E-1
等於
1.00000000000000005551115123126 * (10^-1)
等於
0.100000000000000005551115123126
複製代碼
也就是說:
0.1
//十進制
至關於
(-1)^0 * 2^(-4) * (1.1001100110011001100110011001100110011001100110011010)
//十進制的值
至關於
0011111110111001100110011001100110011001100110011001100110011010
//Double(雙精度)
至關於
0.100000000000000005551115123126
//十進制!
因此用一句話來解釋爲何JS有精度問題:
簡潔版:
由於JS採用Double(雙精度浮點數)來存儲number,Double的小數位只有52位,但0.1等小數的二進制小數位有無限位,因此當存儲52位時,會丟失精度!
考慮周到版:
由於JS採用Double(雙精度浮點數)來存儲number,Double的小數位只有52位,但除最後一位爲5的十進制小數外,其他小數轉爲二進制均有無限位,因此當存儲52位時,會丟失精度!
驗證下Double值0011111110111001100110011001100110011001100110011001100110011010
是否等於十進制0.100000000000000005551115123126
:
根據十進制與 Double 的相互轉換公式得:
//V = (-1)^S * 2^(E-1023) * (1.M)
//S = 0
//E = 119
//M = 1001100110011001100110011001100110011001100110011010
V = (-1)^0 * 2^(-4) * (1.1001100110011001100110011001100110011001100110011010)
//1.1001100110011001100110011001100110011001100110011010的 Double 值計算過程
//S = 0
//E = 1023,二進制爲 01111111111
//M = 1001100110011001100110011001100110011001100110011010
//SEM=0011111111111001100110011001100110011001100110011001100110011010
//轉爲十進制:1.60000000000000008881784197001E0
= 0.0625 * 1.60000000000000008881784197001
複製代碼
用 BigInt 類型來相乘:
625n * 160000000000000008881784197001n
等於
100000000000000005551115123125625n
加上小數點後 33 位,等於
0.100000000000000005551115123125625
發現是四捨五入後的結果,也就是同樣的
0.100000000000000005551115123126
複製代碼
結果一致,驗證正確!
同理,將 0.2 轉爲二進制(過程略,輪到你來練練手了):
0011 0011 0011 無限循環 0011
複製代碼
Double:
//注意第 53 位是 1,須要進位!
(-1)^0 * 2^(-3) * (1. 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010)
S = 0
E = 1020,二進制爲 01111111100
M = 1001100110011001100110011001100110011001100110011010
SEM = 0011111111001001100110011001100110011001100110011001100110011010
複製代碼
經過 Double相互轉換十進制(它是我找獲得的有效位數最多的網站) 得:
2.00000000000000011102230246252E-1
等於
0.200000000000000011102230246252
複製代碼
也就是說:
0.2
//十進制
至關於
(-1)^0 * 2^(-3) * (1.1001100110011001100110011001100110011001100110011010)
//十進制的值
至關於
0011111111001001100110011001100110011001100110011001100110011010
//Double(雙精度)
至關於
0.200000000000000011102230246252
//十進制!
用 BigInt 類型來相加:
100000000000000005551115123126n + 200000000000000011102230246252n
等於
300000000000000016653345369378n
加上小數點一位
0.300000000000000016653345369378
複製代碼
等等!好像不等於0.30000000000000004
?0.30000000000000001 6653345369378
保留小數點後 17 位得:0.30000000000000001
再次驗證:
0.1 = (-1)^0 * 2^(-4) * (1.1001100110011001100110011001100110011001100110011010)
= 0.00011001100110011001100110011001100110011001100110011010
0.2 = (-1)^0 * 2^(-3) * (1.1001100110011001100110011001100110011001100110011010)
= 0.0011001100110011001100110011001100110011001100110011010
0.00011001100110011001100110011001100110011001100110011010 +
0.0011001100110011001100110011001100110011001100110011010 =
0.01001100110011001100110011001100110011001100110011001110
複製代碼
二者相加,結果爲:0.01 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 10
轉化爲 Double,即 SEM:
(-1)^0 * 2^(-2) * (1.0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0100 )
S = 0
E = 1021,二進制爲 01111111101
最後的 10 被舍掉,而且進位
M = 0011001100110011001100110011001100110011001100110100
SEM = 0011111111010011001100110011001100110011001100110011001100110100
複製代碼
經過 Double相互轉換十進制(它是我找獲得的有效位數最多的網站) 得:
3.00000000000000044408920985006E-1
等於
0.30000000000000004 4408920985006
複製代碼
保留小數點後 17 位得:
0.30000000000000004
複製代碼
能夠看到,兩種不一樣的計算過程,致使了計算結果的誤差,我製做了一張流程圖幫助你們理解:
顯然,JavaScript 是按照「驗證方法二」去計算 0.1+0.2 的值的,我有兩個疑問:
① 爲何不用偏差更小的「驗證方法一」呢?
這個我暫時不知道,有大佬知道的話麻煩給我留言。。
② 爲何「驗證方法二」的結果偏差比較大?
蹊蹺在 二進制小數相加轉成 Double 的過程 上,也就是捨去 53 位,並進位會致使偏差:
進位後的 SEM
SEM = 0011111111010011001100110011001100110011001100110011001100110100
轉爲十進制
V = 0.300000000000000044408920985006
若是不進位的話
SEM = 0011111111010011001100110011001100110011001100110011001100110011
轉爲十進制
V = 0.299999999999999988897769753748
複製代碼
發現仍是對不上「驗證一」的結果,緣由仍是在於 Double 的小數位只能保留到 52 位,截取超出的位數不可避免地會致使偏差,而且較大!
網上找的關於0.1+0.2=0.30000000000000004
的文章都是寫的「驗證方法二」,我也不知道本身的「驗證方法一」是否有錯誤,懇請看到的讀者加以指正。
問題 ② 算解決了,問題 ① 暫不解決,我太累了。。
最後:
感謝你的耐心看完了這篇文章,麻煩給文中參考的文章點個贊,沒有他們也不會有這篇文章的誕生,謝謝!
(完)