前言
0.1 + 0.2 是否等于 0.3 作為一道經(jīng)典的面試題,已經(jīng)廣外熟知,說(shuō)起原因,大家能回答出這是浮點(diǎn)數(shù)精度問(wèn)題導(dǎo)致,也能辯證的看待這并非是 ECMAScript 這門語(yǔ)言的問(wèn)題,今天就是具體看一下背后的原因。
數(shù)字類型
ECMAScript 中的 Number 類型使用 IEEE754 標(biāo)準(zhǔn)來(lái)表示整數(shù)和浮點(diǎn)數(shù)值。所謂 IEEE754 標(biāo)準(zhǔn),全稱 IEEE 二進(jìn)制浮點(diǎn)數(shù)算術(shù)標(biāo)準(zhǔn),這個(gè)標(biāo)準(zhǔn)定義了表示浮點(diǎn)數(shù)的格式等內(nèi)容。
在 IEEE754 中,規(guī)定了四種表示浮點(diǎn)數(shù)值的方式:?jiǎn)尉_度(32位)、雙精確度(64位)、延伸單精確度、與延伸雙精確度。像 ECMAScript 采用的就是雙精確度,也就是說(shuō),會(huì)用 64 位來(lái)儲(chǔ)存一個(gè)浮點(diǎn)數(shù)。
浮點(diǎn)數(shù)轉(zhuǎn)二進(jìn)制
我們來(lái)看下 1020 用十進(jìn)制的表示:
1020 =?1?* 10^3 +?0?* 10^2 +?2?* 10^1 +?0?* 10^0
所以 1020 用十進(jìn)制表示就是 1020……(哈哈)
如果 1020 用二進(jìn)制來(lái)表示呢?
1020 =?1?* 2^9 +?1?* 2^8 +?1?* 2^7 +?1?* 2^6 +?1?* 2^5 +?1?* 2^4 +?1?* 2^3 +?1?* 2^2 +?0?* 2^1 +?0?* 2^0
所以 1020 的二進(jìn)制為?1111111100
那如果是 0.75 用二進(jìn)制表示呢?同理應(yīng)該是:
0.75 = a * 2^-1 + b * 2^-2 + c * 2^-3 + d * 2^-4 + ...
因?yàn)槭褂玫氖嵌M(jìn)制,這里的 abcd……的值的要么是 0 要么是 1。
那怎么算出 abcd…… 的值呢,我們可以兩邊不停的乘以 2 算出來(lái),解法如下:
0.75 = a * 2^-1 + b * 2^-2 + c * 2^-3 + d * 2^-4...
兩邊同時(shí)乘以 2
1 + 0.5 = a * 2^0 + b * 2^-1 + c * 2^-2 + d * 2^-3... (所以 a = 1)
剩下的:
0.5 = b * 2^-1 + c * 2^-2 + d * 2^-3...
再同時(shí)乘以 2
1 + 0 = b * 2^0 + c * 2^-2 + d * 2^-3... (所以 b = 1)
所以 0.75 用二進(jìn)制表示就是 0.ab,也就是 0.11
然而不是所有的數(shù)都像 0.75 這么好算,我們來(lái)算下 0.1:
0.1 = a * 2^-1 + b * 2^-2 + c * 2^-3 + d * 2^-4 + ...
0 + 0.2 = a * 2^0 + b * 2^-1 + c * 2^-2 + ...? (a = 0)
0 + 0.4 = b * 2^0 + c * 2^-1 + d * 2^-2 + ...? (b = 0)
0 + 0.8 = c * 2^0 + d * 2^-1 + e * 2^-2 + ...? (c = 0)
1 + 0.6 = d * 2^0 + e * 2^-1 + f * 2^-2 + ...? (d = 1)
1 + 0.2 = e * 2^0 + f * 2^-1 + g * 2^-2 + ...? (e = 1)
0 + 0.4 = f * 2^0 + g * 2^-1 + h * 2^-2 + ...? (f = 0)
0 + 0.8 = g * 2^0 + h * 2^-1 + i * 2^-2 + ...? (g = 0)
1 + 0.6 = h * 2^0 + i * 2^-1 + j * 2^-2 + ...? (h = 1)
....
然后你就會(huì)發(fā)現(xiàn),這個(gè)計(jì)算在不停的循環(huán),所以 0.1 用二進(jìn)制表示就是 0.00011001100110011……
浮點(diǎn)數(shù)的存儲(chǔ)
雖然 0.1 轉(zhuǎn)成二進(jìn)制時(shí)是一個(gè)無(wú)限循環(huán)的數(shù),但計(jì)算機(jī)總要儲(chǔ)存吧,我們知道 ECMAScript 使用 64 位來(lái)儲(chǔ)存一個(gè)浮點(diǎn)數(shù),那具體是怎么儲(chǔ)存的呢?這就要說(shuō)回 IEEE754 這個(gè)標(biāo)準(zhǔn)了,畢竟是這個(gè)標(biāo)準(zhǔn)規(guī)定了存儲(chǔ)的方式。
這個(gè)標(biāo)準(zhǔn)認(rèn)為,一個(gè)浮點(diǎn)數(shù) (Value) 可以這樣表示:
Value = sign * exponent * fraction
看起來(lái)很抽象的樣子,簡(jiǎn)單理解就是科學(xué)計(jì)數(shù)法……
比如 -1020,用科學(xué)計(jì)數(shù)法表示就是:
-1 * 10^3 * 1.02
sign 就是 -1,exponent 就是 10^3,fraction 就是 1.02
對(duì)于二進(jìn)制也是一樣,以 0.1 的二進(jìn)制 0.00011001100110011…… 這個(gè)數(shù)來(lái)說(shuō):
可以表示為:
1 * 2^-4 * 1.1001100110011……
其中 sign 就是 1,exponent 就是 2^-4,fraction 就是 1.1001100110011……
而當(dāng)只做二進(jìn)制科學(xué)計(jì)數(shù)法的表示時(shí),這個(gè) Value 的表示可以再具體一點(diǎn)變成:
V = (-1)^S * (1 + Fraction) * 2^E
(如果所有的浮點(diǎn)數(shù)都可以這樣表示,那么我們存儲(chǔ)的時(shí)候就把這其中會(huì)變化的一些值存儲(chǔ)起來(lái)就好了)
我們來(lái)一點(diǎn)點(diǎn)看:
(-1)^S?表示符號(hào)位,當(dāng) S = 0,V 為正數(shù);當(dāng) S = 1,V 為負(fù)數(shù)。
再看?(1 + Fraction),這是因?yàn)樗械母↑c(diǎn)數(shù)都可以表示為 1.xxxx * 2^xxx 的形式,前面的一定是 1.xxx,那干脆我們就不存儲(chǔ)這個(gè) 1 了,直接存后面的 xxxxx 好了,這也就是 Fraction 的部分。
最后再看?2^E
如果是 1020.75,對(duì)應(yīng)二進(jìn)制數(shù)就是 1111111100.11,對(duì)應(yīng)二進(jìn)制科學(xué)計(jì)數(shù)法就是 1 * 1.11111110011 * 2^9,E 的值就是 9,而如果是 0.1 ,對(duì)應(yīng)二進(jìn)制是 1 * 1.1001100110011…… * 2^-4, E 的值就是 -4,也就是說(shuō),E 既可能是負(fù)數(shù),又可能是正數(shù),那問(wèn)題就來(lái)了,那我們?cè)撛趺磧?chǔ)存這個(gè) E 呢?
我們這樣解決,假如我們用 8 位來(lái)存儲(chǔ) E 這個(gè)數(shù),如果只有正數(shù)的話,儲(chǔ)存的值的范圍是 0 ~ 254,而如果要儲(chǔ)存正負(fù)數(shù)的話,值的范圍就是 -127~127,我們?cè)诖鎯?chǔ)的時(shí)候,把要存儲(chǔ)的數(shù)字加上 127,這樣當(dāng)我們存 -127 的時(shí)候,我們存 0,當(dāng)存 127 的時(shí)候,存 254,這樣就解決了存負(fù)數(shù)的問(wèn)題。對(duì)應(yīng)的,當(dāng)取值的時(shí)候,我們?cè)贉p去 127。
所以呢,真到實(shí)際存儲(chǔ)的時(shí)候,我們并不會(huì)直接存儲(chǔ) E,而是會(huì)存儲(chǔ) E + bias,當(dāng)用 8 位的時(shí)候,這個(gè) bias 就是 127。
所以,如果要存儲(chǔ)一個(gè)浮點(diǎn)數(shù),我們存 S 和 Fraction 和 E + bias 這三個(gè)值就好了,那具體要分配多少個(gè)位來(lái)存儲(chǔ)這些數(shù)呢?IEEE754 給出了標(biāo)準(zhǔn):
在這個(gè)標(biāo)準(zhǔn)下:
我們會(huì)用 1 位存儲(chǔ) S,0 表示正數(shù),1 表示負(fù)數(shù)。
用 11 位存儲(chǔ) E + bias,對(duì)于 11 位來(lái)說(shuō),bias 的值是 2^(11-1) - 1,也就是 1023。
用 52 位存儲(chǔ) Fraction。
舉個(gè)例子,就拿 0.1 來(lái)看,對(duì)應(yīng)二進(jìn)制是 1 * 1.1001100110011…… * 2^-4, Sign 是 0,E + bias 是 -4 + 1023 = 1019,1019 用二進(jìn)制表示是 1111111011,F(xiàn)raction 是 1001100110011……
對(duì)應(yīng) 64 位的完整表示就是:
0 01111111011 1001100110011001100110011001100110011001100110011010
同理, 0.2 表示的完整表示是:
0 01111111100 1001100110011001100110011001100110011001100110011010
所以當(dāng) 0.1 存下來(lái)的時(shí)候,就已經(jīng)發(fā)生了精度丟失,當(dāng)我們用浮點(diǎn)數(shù)進(jìn)行運(yùn)算的時(shí)候,使用的其實(shí)是精度丟失后的數(shù)。
浮點(diǎn)數(shù)的運(yùn)算
關(guān)于浮點(diǎn)數(shù)的運(yùn)算,一般由以下五個(gè)步驟完成:對(duì)階、尾數(shù)運(yùn)算、規(guī)格化、舍入處理、溢出判斷。我們來(lái)簡(jiǎn)單看一下 0.1 和 0.2 的計(jì)算。
首先是對(duì)階,所謂對(duì)階,就是把階碼調(diào)整為相同,比如 0.1 是?1.1001100110011…… * 2^-4,階碼是 -4,而 0.2 就是?1.10011001100110...* 2^-3,階碼是 -3,兩個(gè)階碼不同,所以先調(diào)整為相同的階碼再進(jìn)行計(jì)算,調(diào)整原則是小階對(duì)大階,也就是 0.1 的 -4 調(diào)整為 -3,對(duì)應(yīng)變成?0.11001100110011…… * 2^-3
接下來(lái)是尾數(shù)計(jì)算:
? 0.1100110011001100110011001100110011001100110011001101
+ 1.1001100110011001100110011001100110011001100110011010
————————————————————————————————————————————————————————
10.0110011001100110011001100110011001100110011001100111
我們得到結(jié)果為?10.0110011001100110011001100110011001100110011001100111 * 2^-3
將這個(gè)結(jié)果處理一下,即結(jié)果規(guī)格化,變成?1.0011001100110011001100110011001100110011001100110011(1) * 2^-2
括號(hào)里的 1 意思是說(shuō)計(jì)算后這個(gè) 1 超出了范圍,所以要被舍棄了。
再然后是舍入,四舍五入對(duì)應(yīng)到二進(jìn)制中,就是 0 舍 1 入,因?yàn)槲覀円牙ㄌ?hào)里的 1 丟了,所以這里會(huì)進(jìn)一,結(jié)果變成
1.0011001100110011001100110011001100110011001100110100 * 2^-2
本來(lái)還有一個(gè)溢出判斷,因?yàn)檫@里不涉及,就不講了。
所以最終的結(jié)果存成 64 位就是
0 01111111101 0011001100110011001100110011001100110011001100110100
將它轉(zhuǎn)換為10進(jìn)制數(shù)就得到?0.30000000000000004440892098500626
因?yàn)閮纱未鎯?chǔ)時(shí)的精度丟失加上一次運(yùn)算時(shí)的精度丟失,最終導(dǎo)致了 0.1 + 0.2 !== 0.3
其他
// 十進(jìn)制轉(zhuǎn)二進(jìn)制parseFloat(0.1).toString(2);=>"0.0001100110011001100110011001100110011001100110011001101"http:// 二進(jìn)制轉(zhuǎn)十進(jìn)制parseInt(1100100,2)=>100// 以指定的精度返回該數(shù)值對(duì)象的字符串表示(0.1+0.2).toPrecision(21)=>"0.300000000000000044409"(0.3).toPrecision(21)=>"0.299999999999999988898