JavaScript中的數(shù)字運算大概是最讓人迷惑的了,我看過許多講述JS怪異之處的資料都會舉一個列子:
0.1 + 0.2; // 0.30000000000000004
除了跟風喊幾句什么鬼之外,讓我們一起來探索一下隱藏在背后的東西。在計算機的世界,有果必有因。
首先要明確一個概念,JavaScript中的所有數(shù)字都是浮點數(shù),并且是符合IEEE754標準的雙精度浮點數(shù)。一般我們看到的類似整數(shù)的東西其實都是浮點數(shù),只不過是小數(shù)點后沒有數(shù)字,不顯示小數(shù)部分而已。
數(shù)字字面量
盡管語言內(nèi)部表示只有浮點數(shù),但數(shù)字字面量表示還是可以有整數(shù)和浮點數(shù)的。因此我們可以直接輸入35,而不用35.0來表示這個數(shù)字。
JS中的數(shù)字還有幾個特殊值:
- 表示錯誤的值:NaN和Infinity
- 用于某些數(shù)學計算的+0和-0
這幾個特殊值很容易讓人摸不清頭腦,他們本身也有許多反直覺的地方,且讓我細細道來。
NaN
它是Not A Number的縮寫,表示值不是數(shù)字。比如嘗試將'aa'解析為數(shù)字:
Number('aa'); // NaN
看起來好像很簡單,但這里有不少陷阱,一不小心就會給你帶來難以察覺的bug。首先雖然NaN表示的是不是數(shù)字,但如果我們用typeof檢查它自身的類型:
typeof NaN; //Number
表示不是數(shù)字的東西自身卻是個數(shù)字,這個邏輯讓我不敢直視。
然后如果我們嚴格比較兩個NaN:
NaN === NaN //false
好吧,明明是一樣的東西,類型也同是Number,居然是不相等的!NaN也是JS中唯一一個不等于自身的值。所有要判斷某個值是否是NaN不能直接比較,而要用原生的isNaN方法:
isNaN(NaN); //true
這還沒有完,如果你將非數(shù)字字面量傳入isNaN方法,你很可能得到的時true!
isNaN('aaa'); //true
可'aaa'明明不是NaN??!這是因為'aaa'先被隱性轉換成數(shù)字,然后再傳入isNaN,也就是類似:
isNaN(Number('aaa'));
前面提過,Number('aaa')的結果是NaN,所以isNaN自然為true。要躲過這個陷阱,必須在判斷時多加一個判斷是否是數(shù)字的條件:
if (typeof value === 'number' && isNaN(value))
還有一個更簡單的方法,利用前面提到的NaN是唯一一個不和自己相等的東西:
if (value !== value)
于是只要value是NaN,這里就一定為true,其他情況則一律為false。
Infinity
再來看看Infinity,這個值通常用來表示一個超出表示范圍的值,當一個數(shù)字除0的時候也會返回Infinity。Infinity同樣有+Infinity和-Infinity。比起NaN,Infinity要友善的多,你可以直接用===來判斷,也可以用內(nèi)置的isFinite()來做判斷。
+0/- 0
最后再來看看正零和負零。這樣反常識的表示方法在某些數(shù)學運算領域是很有用的,比如在表示趨于零的極限時,正零和負零可以幫助我們表示趨近的方向。但一般情況下,我們可粗略的認為只有一個0,不需要做特別的區(qū)分。
(-0).toString() //'0' (+0).toString() //'0'
數(shù)字的內(nèi)部表示
本文的開頭說過,JavaScipt中的所有數(shù)字都是64位的雙精度浮點數(shù)。這里我們就來詳細看看這種浮點數(shù)在內(nèi)部是如何表示的,以及這種表示方法的一些問題和局限。
JS中的浮點數(shù)由三部分組成:
- 符號占1位
- 指數(shù)部分占11位
- 小數(shù)部分占52位
這三部分加起來就是64位了。

而計算機內(nèi)部都是二進制實現(xiàn)的,所以數(shù)字計算公式為:
(–1)sign × %1.fraction × 2exponent
%表示二進制。
這樣的表示方法有什么問題呢?這就是開篇提出的那個例子:
0.1 + 0.2; // 0.30000000000000004
更奇怪的是,按常識我們都知道加法的結合律,既(a+b)+c = a+(b+c)。但在JS中這個公里不成立:
(0.1 + 0.2) + 0.3; // 0.6000000000000001 0.1 + (0.2 + 0.3); // 0.6
讓我們來詳細探討一下這個問題是怎么產(chǎn)生的。我們先來看看我們習慣使用的十進制。十進制的小數(shù)可以用分數(shù)的形式表示:m/10^e
可以看到,分母部分都是十的次方。十進制也有不能精確表示的分數(shù),比如1/3就不能被精確表示。這是因為分母不含3,因此必然無法表示為10^e。
再來看二進制,同樣的分數(shù)表示法,只不過分母是2的次方而已。因此我們可以得出結論,只要分母部分不是2的次方的,都無法精確表示為2^e:
0.5dec = 5/10 = 1/2 = 01bin
0.75dec = 75/100 = 3/4 = 0.11bin
0.1dec = 1/10 = 1/2X5
0.2dec = 2/10 = 1/5
現(xiàn)在你明白為什么 0.1 + 0.2是0.30000000000000004了吧。我們在console里看到的0.1并不是完整的內(nèi)部表示,稍微做點處理就能看到內(nèi)部表示了:
0.1 * Math.pow(10, 24) //1.0000000000000001e+23
那么在需要精確計算的時候有什么方法嗎?有的,那就是使用整數(shù)。整數(shù)沒有小數(shù)部分的舍入問題,可以準確地被表示。例如在金融方面,可以按最小單位的整數(shù)來表示錢。比如0.55元表示為55分,按55來進行計算。
另外也要注意,直接比較小數(shù)可能會帶來不可知的結果,最好使用Machine_epsilon來進行比較:
var EPSILON = Math.pow(2, -53);
function epsEqu(x, y) {
return Math.abs(x - y) < EPSILON;
}
整數(shù)
前面說過,JS中并不存在真正的整數(shù)。整數(shù)都是用浮點數(shù)表示的,但是要注意有一處例外的地方,那就是位運算。JS中的位運算會先將數(shù)字轉換為32位整數(shù),運算完成后返回的結果也是32位整數(shù)。
另外,由于整數(shù)是由浮點數(shù)表示的,這里有一個安全整數(shù)的概念。JS中的安全整數(shù)指的是范圍在(?253, 253)內(nèi)的整數(shù)。我們說他們是安全的,是因為在這個范圍內(nèi)可以保證每個整數(shù)只有一個對應的浮點數(shù)表示形式。超過這個范圍則會出現(xiàn)多個浮點表示形式。因此在JS中做整數(shù)運算時,最好保證運算的整數(shù)都在這個安全范圍之內(nèi)。一定要處理大整數(shù)的話必須依賴相應的類庫,否則結果很可能不準確。
在寫完這篇文章之后,JS之父Brendan Eich透露了在ES7中會支持64位大整數(shù)。JS漸漸擺脫了玩具語言的束縛,向更廣闊的天地出發(fā)了。跟上了,程序員。