Under the Hood: NaN of JavaScript
在檢視本文之前,請先思考兩個問題。
typeof (1 / undefined) [1, 2, NaN].indexOf(NaN)
如果你還不確定這兩題的答案的話,請仔細閱讀本文。 這兩題的答案不會直接解釋,請從文章中尋找答案。
NaN 的本質
我們知道 NaN(Not A Number) 會出現在 任何不符合實數領域內計算規則 的場景下。比如 Math.sqrt(-1)
就是 NaN,而 1 / 0
就不是 NaN。前者屬於複數的範疇,而後者屬於實數的範圍。
同時需要注意的是,NaN 只會出現在浮點型別中,而不會出現在 int 型別裡(當然 JS 並沒有這個概念)
什麼意思?用你熟悉的任何支援 int 和 double 兩種型別的語言(比如 C)。在保證它不會偷偷做隱式型別轉換的情況下,分別用 int 和 double 打印出 sqrt(-1)
, 你就能發現只有在 double 的型別下才能看到 NaN 出現,而 int 呢?編譯器甚至會給你一個 Warning。
那麼在浮點數下是如何表示一個 NaN 的呢?為了方便,下面用單精度 float 來表示,請看下圖。

,這裡不做具體的介紹,我們只需要瞭解到浮點數分為 3 個部分就可以:
- 符號位
- 指數位
- 精度位
其中 float 的指數位有 8 位,精度位有 32 - 1 - 8 = 23 位 double 的指數位有 11 位,精度位有 64 - 1 - 11 = 52 位 所以上面 NaN 的滿足條件,可以看成: 精度位不全為 0,指數位全 1 就可以了。
所以按上面的說法, 0x7f81111, 0x7fcccccc
等等這些都符合 NaN 的要求了。我們可以嘗試一下,自己寫一個函式,用來往 8 個位元組的記憶體的前兩個位元組寫入全 1. 也就是連續 16 個 1,這就符合 NaN 的定義了。看下面這段程式碼:
double createNaN() { unsigned char *bits = calloc(sizeof(double), 1); // 大部分人的電腦是小端,所以要從 6 和 7 開始,而不是 0 和 1 // 不清楚概念的可以參考阮老師: // [理解位元組序 - 阮一峰的網路日誌](http://www.ruanyifeng.com/blog/2016/11/byte-order.html) bits[6] = 255; bits[7] = 255; unsigned char *start = bits; double nan = *(double *)(bits); output(nan); free(bits); return nan; } 複製程式碼
其中 output 是一個封裝,用來輸出任意一個 double 的內部二進位制表示。詳細程式碼檢視 gist 。 最後我們得到了:

看來創造一個 NaN 不是很難,對吧? 同樣的,為了證明上面的圖的正確性,再看看 Infinity
的內部結構是否符合

兩種 NaN
如果再細分的話,NaN 還可分為兩種:
- Quiet NaN
- Signaling NaN
從性質上,可以認為第一種 NaN 屬於“脾氣比較好”,比較“文靜”的一種,你甚至可以直接定義它,並使用它。 比如我們在 JS 中可以使用類似於 NaN + 1, NaN + '123'
的操作,還不會報錯。
而 Signaling NaN 就是一個“爆脾氣”。如果你想直接操作它的話,會丟擲一個異常(或者稱為 Trap)。也就不允許 NaN + 1 這種操作了。像這種不好惹的 NaN,根據 WiKi 中的介紹,它可以被用來:
Filling uninitialized memory with signaling NaNs would produce the invalid operation exception if the data is used before it is initialized Using an sNaN as a placeholder for a more complicatedobject , such as: A representation of a number that hasunderflowed A representation of a number that hasoverflowed Number in a higher precision format Acomplex number
NaN != NaN
如果換個角度理解,因為 NaN 的表示方式實在太多,僅僅在 float 型別中,就有 2^(32-8) 中情況,所以 NaN 碰到一個和它二進位制表示一模一樣的概率實在太低了,所以我們可以認為 NaN 不等於 NaN :smirk:
嗯。看上去似乎問題不大,但是我們都知道計算機在大多數情況下,都是按規矩辦事,這種玄學問題肯定不是內部的本質吧?要是真這樣,世界上每一個程式員同時輸出 NaN === NaN
,總有一個人會得到 true,然後他就到 stackoverflow 上發了一個帖: 你看 NaN 其實是會等於 NaN 的! 但我們從來沒有見過這樣的帖子,所以計算機內部肯定不是用這種頗為靠運氣的方式在處理這個問題。
考慮換一種方式,假設計算機內部是通過 位運算 來判斷的。如果某一個數的內部結構滿足 第 2 位到第 9 位全 1,剩下的 22 位不為 0 ,那它就是 NaN。我們可以這樣寫
_Bool isnan(double whatever) { long long num = *(long long *)(&whatever); // 浮點數不能進行位運算,所以要改成整數型別,同時保留內部的二進位制組成 long long fmask = 0xfffffffffffff; // 不要數了,13 個 f,52 個 1 long long emask = 0x7ff; // 11 個 1 num <<= 1; num >>= 1; // 清除符號位 return ((num & fmask) != 0) && (((num >> 53) & emask) == emask); } 複製程式碼
你可以試著把這段 C 程式碼執行一下,配合上面的 createNaN
可以試一下,他是真的可行的!
接著要實現 NaN != NaN 的特性,只需要在每次 == 的時候進行檢測:只要有一個運算元是 NaN,那麼就返回 false。
實際情況下的 NaN != NaN 的實現
那麼實際情況到底是怎樣的呢?不同的系統會有不同的實現。
在 Apple 實現的C 庫的標頭檔案中,可以看到,nan 在 float 下,僅僅就是一個數,它等於 0x7fc00000 ,也就是 0b0111 1111 1100 0000 0000 0000 0000 0000 ,符合上面的 NaN 的定義。 #define NAN __builtin_nanf("0x7fc00000")
而它們的 isnan
的實現也相當簡單
#define isnan(x)\ (sizeof (x) == sizeof(float)\ ? __inline_isnanf((float)(x))\ : sizeof (x) == sizeof(double) \ ? __inline_isnand((double)(x))\ : __inline_isnan ((long double)(x))) static __inline__ int __inline_isnanf( float __x ) { return __x != __x; } static __inline__ int __inline_isnand( double __x ) { return __x != __x; } static __inline__ int __inline_isnan( long double __x ) { return __x != __x; } 複製程式碼
僅僅只是簡單的判斷自己是否等於自己 :new_moon_with_face:。在 C 中具體如何實現 x !== x
,有兩種可能:
- 硬體支援 NaN 異常,所以永遠都是 false
- 像下文中提到的 V8 的實現方式
而在 V8 中,分為兩個階段:/Compile Time and Runtime/。
在 Compile Time,編譯器如果在程式碼中碰到了 NaN 常量,就會自動將替換成 NaN 對應的那個常量,比如上文提到的 0x7fc00000 。因為編譯器已經明確知道了誰是 NaN,所以在寫出形如 NaN === NaN
這種程式碼的時候,就能直接得到 false。
而在 Runtime 階段,不是使用者直接定義的 NaN,比如下面程式碼:
const obj = { a: 1, b: 2 }; let { c, d } = obj; c *= 100; d *= 100; console.log(c === d); 複製程式碼
這種情況下,我們雖然一眼可以看出最後的 c 和 d 都是 undefined,但是編譯器剛開始不知道,所以它只能在最後判等的時候,才能得到結果。而具體判斷的邏輯如下圖所示: 我們先檢查,運算元是否有 NaN,如果有?那就返回 false 吧

所以 Number.isNaN
的 polyfill 可以怎麼實現呢?
Number.isNaN = function(value) { return value !== value; } 複製程式碼
就是這麼簡單 :sunglasses: