在檢視本文之前,請先思考兩個問題。
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 來表示,請看下圖。
在 3b 情況中,NaN 得滿足:從左到右,以 1 開始,不關心第 1 位的值,第 2 位到第 9 位都是 1,剩下的位不全 為 0。 關於 浮點數內部的組成,這裡不做具體的介紹,我們只需要瞭解到浮點數分為 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 complicated object , such as: A representation of a number that has underflowed A representation of a number that has overflowed Number in a higher precision format A complex number
NaN != NaN
如果換個角度理解,因為 NaN 的表示方式實在太多,僅僅在 float 型別中,就有 2^(32-8) 中情況,所以 NaN 碰到一個和它二進位制表示一模一樣的概率實在太低了,所以我們可以認為 NaN 不等於 NaN ?
嗯。看上去似乎問題不大,但是我們都知道計算機在大多數情況下,都是按規矩辦事,這種玄學問題肯定不是內部的本質吧?要是真這樣,世界上每一個程式設計師同時輸出 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;
}
複製程式碼
僅僅只是簡單的判斷自己是否等於自己 ?。在 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;
}
複製程式碼
就是這麼簡單 ?