平時在寫js代碼時會用到一些簡單的計算,比方說系統中咱們數據庫儲存的金額是分,前端展現的是元,因此在用戶輸入元以後要轉成分傳給後臺,這個公式小學一年級就學過了html
1.11*100 = 111
複製代碼
通常來講這個計算結果是沒問題的,可是在js裏面卻有這樣的尷尬前端
1.11*100 = 111.00000000000001
複製代碼
結果不是咱們想要的111,相似的狀況還有git
0.1+0.2 = 0.30000000000000004 //加法
0.27-0.11 = 0.16000000000000003 //減法
19.9*100 = 1989.9999999999998 //乘法
0.3/0.1 = 2.9999999999999996 //除法
複製代碼
通常遇到這種問題,咱們都有成熟的解決方案解決github
用着用着就習慣了,一直沒有搞清楚爲何會有這樣的偏差。這兩天正好有空,看了一些博客終於搞清楚了。數據庫
JS 的數據類型比較特別,和C、Java
等語言的的數據類型不同,無論是 int、double、float
在 JS 裏面都是Number
類型。
要搞清楚爲何有這個偏差,就要先介紹一下雙精度浮點(double)json
雙精度浮點數(double)使用 64 位(8字節) 來儲存一個浮點數。 它能夠表示十進位制的15或16位有效數字,其能夠表示的數字的絕對值範圍大約是 [2.2310^(-308),1.7910^(308)]。bash
這其中的64位bit又能夠分爲下面的格式spa
上面的格式能夠轉換成這個這個公式 3d
在十進制中,整數部分能夠是0~9,在二進制中整數部分只能是0~1,因此能夠看到上面公式對應的整數部分只能是1,這樣就能夠不用管整數部分直接保留後面的小數部分就能夠了。指數 exponent(E) 是一個無符號整型 (unsigned int) ,那麼問題就來了,咱們怎麼保留小數呢?按照科學計數法,若是E小於0才能夠表示成小數,由於E是11位的,最大能夠表示爲2047,因此取一箇中間值1023(十六進制爲ox3FF),0~1022表示爲負,1023~2047表示爲正,這樣就解決了小數的表示問題。code
咱們來看看數字 1 是怎麼儲存的
用上面的公式表示就是:(-1)^0 * 2^(1024-1023) * 1.0 = 1,再看一下 0.5 的儲存形式
(-1)^0 * 2^(1022-1023) * 1.0 = 0.5,搞清楚這個,咱們再看看上面提到的 1.11*100 = 111.00000000000001
這個問題。
將 1.11 轉換成二進制是這樣的1.0001110000101000111101011100001010001111010111000011...
(11100001010001111010循環)(十進制小數轉二進制方法),換成64位浮點來表示,S爲0,E爲1023,mantissa(M)爲0001110000101000111101011100001010001111010111000011,由於位數只有52位,後面循環的部分就被捨棄了,轉成64位浮點數是這樣的
1.11000000000000009769962616701
複製代碼
因此這裏出現了問題,偏差就有了,究其根本仍是精度的問題。
爲何我直接輸入 1.11 獲得的結果是 1.11,而不是1.11000000000000009769962616701 呢?
這個仍是精度問題,64位浮點的尾數是52位,由於整數部分只能是1因此能夠省略一位,比方說
11.101 * 2^1001 能夠格式化爲 1.1101 * 2^1010,尾數部分M直接儲存1101便可;
0.0011101 * 2^-1001 能夠格式化爲 1.1101 * 2^-1100,尾數部分M儲存1101便可。
複製代碼
因此他能夠表示的最大長度是53,即2^53 = 9007199254740992,因此雙精度浮點能表示的最大精度是 16 位的,JS 會調用 toPrecision(16) 來作運算
1.11.toPrecision(16) = 1.110000000000000 //自動取整以後就是1.11
複製代碼
若是精度調整一下,結果就不同了:
1.11.toPrecision(17) = 1.1100000000000001
1.11.toPrecision(20) = 1.1100000000000000977
複製代碼
到這裏終於真相大白了!