JavaScript之0.1+0.2=0.30000000000000004的計算過程

前言:
在看了 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 無限循環0011git

採用科學計數法,表示 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的文章都是寫的「驗證方法二」,我也不知道本身的「驗證方法一」是否有錯誤,懇請看到的讀者加以指正。

問題 ② 算解決了,問題 ① 暫不解決,我太累了。。

最後:
感謝你的耐心看完了這篇文章,麻煩給文中參考的文章點個贊,沒有他們也不會有這篇文章的誕生,謝謝!


(完)

相關文章
相關標籤/搜索