Under the Hood: NaN of JavaScript

retailfe_youzan發表於2018-10-12

在檢視本文之前,請先思考兩個問題。

  1. typeof (1 / undefined) 是多少
  2. [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 來表示,請看下圖。

Under the Hood: NaN of JavaScript
在 3b 情況中,NaN 得滿足:從左到右,以 1 開始,不關心第 1 位的值,第 2 位到第 9 位都是 1,剩下的位不全 為 0。 關於 浮點數內部的組成,這裡不做具體的介紹,我們只需要瞭解到浮點數分為 3 個部分就可以:

  1. 符號位
  2. 指數位
  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。 最後我們得到了:

Under the Hood: NaN of JavaScript

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

Under the Hood: NaN of JavaScript


兩種 NaN

如果再細分的話,NaN 還可分為兩種:

  1. Quiet NaN
  2. 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,有兩種可能:

  1. 硬體支援 NaN 異常,所以永遠都是 false
  2. 像下文中提到的 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 吧

Under the Hood: NaN of JavaScript

所以 Number.isNaN 的 polyfill 可以怎麼實現呢?

Number.isNaN = function(value) {
  return value !== value;
}
複製程式碼

就是這麼簡單 ?

參考文獻

相關文章