問題
在javascript使用小數點 +-*/
運算會出現誤差,比如:
0.1 + 0.2 == 0.30000000000000004
0.2 + 0.4 == 0.6000000000000001
19.9 * 100 == 1989.9999999999998
其實,不僅僅只有javascript,還有java、python等都會有類似問題,因為浮點數IEEE754是被普遍使用的標准
浮點數
浮點數是相對於定點數來說的。
計算機中小數的表示法,有定點和浮點兩種。
定點,即小數點固定,比如:302876512411.25
小數點固定在數字的個位數右邊
浮點,即小數點不固定(浮動),3.028e+11
小數點不固定在個位數和小數之間,而是根據指數值進行前后浮動
可以這么理解,科學記數法就是浮點數的表示方式
那么,我們為什么要使用浮點數呢?
我們可以先考慮下,為什么要使用科學記數法?
302876512411.25
3.028e+11
科學記數法的核心就是:通過移動小數點,只在小數點前保留一位數字,其他都算作小數,並使用指數記錄小數點移動的位數
好處是,通過指數就可以很直觀的看出數值的大小(而不用個十百千萬的數)
其實它還有另一個好處,如果省略小數點后幾位的話,它會顯得很廋,不像原值那么臃腫(缺點是精度丟失)
計算機中的浮點數
因為計算機中,數值的存儲是有大小限制的,比如
單精度浮點數 float -- 4Byte -- 32bit
雙精度浮點數 double -- 8Byte -- 64bit
問題: 如何在有限的存儲空間內容,盡可能的表示更多的數值?
使用定點數(原值),雖然保留了精度,但是能夠表示的數值范圍有限
使用浮點數(科學記數法),能夠表示的數值范圍變廣了,但是精度也丟失了
也就是說,相同位數下
范圍和精度是不可兼得
兩害相權取其輕,微小的精度 沒有 數值表示范圍 顯得更重要
所以計算機中表示小數的方式就是使用了浮點數,也就有了 IEEE754標准
二進制的定點和浮點
定點數: 以32位存儲為例 (124 + 1*2e-2)
圖片來源: https://www.zhihu.com/question/19848808
浮點數:(1.01)2 = 1.25 即 1.25 * 2e-3
圖片來源: https://www.zhihu.com/question/19848808
階碼=階碼真值+127。 (127是單精度浮點的偏移量,即 0111 1111)
IEEE 754
浮點數的存儲格式,一般按照標准IEEE 754。
IEEE 754 規定,浮點數的表示方法為:
最高的 1 位是符號位 s,(表示正負)
接着的 8 位是階碼真值E,(補碼,計算真值需要加上偏移量)
剩下的 23 位為尾數 M。(原碼)
即
float: 1 + 8 + 23 = 32
double: 1 + 11 + 52 = 64
例如:0.125 DEC = 1/8 = 0.001 BIN = 1 x 2^-3
= 0 + 0111 1100 + 0000 0000 0000 0000 0000 000 = 00111110000000000000000000000000
IEEE754換算工具: http://www.binaryconvert.com/convert_double.html
浮點數加減乘除運算出現誤差的原因
0.125 = 1/8
0.0625 = 1/16
0.03125 = 1/32
0.015625 = 1/64
0.0078125 = 1/128
0.00390625 = 1/256
0.1 = 0/2 + 0/4 + 0/8 + 1/16 + 1/32 + 0/64 + 0/128 + 1/256 + 1/512 + ...
0.1 = 0.0001 1001 1001 ...
0.2 = 0/2 + 0/4 + 1/8 + 1/16 + 0/32 + 0/64 + 1/128 + 1/256 + 0/512 + ...
0.1 = 0.0011 0011 0011 ...
原因: 一個能准確表示的十進制小數,而二進制卻是循環小數
解決方法
避免使用會清除小數的換算方式
Math.floor
Math.ceil
對於整數,一般不會出錯
對於小數,出錯的概率較高,可以先變為整數,再縮為小數 (0.1*10 + 0.2*10) / 10 == 0.3
小數的加減乘除運算可以封裝方法: https://blog.51cto.com/xzllff/831241