Under the Hood: NaN of JavaScript

在查看本文以前,請先思考兩個問題。html

  1. typeof (1 / undefined) 是多少
  2. [1, 2, NaN].indexOf(NaN) 輸出什麼

若是你還不肯定這兩題的答案的話,請仔細閱讀本文。 這兩題的答案不會直接解釋,請從文章中尋找答案。git

NaN 的本質

咱們知道 NaN(Not A Number) 會出如今任何不符合實數領域內計算規則的場景下。好比 Math.sqrt(-1) 就是 NaN,而 1 / 0 就不是 NaN。前者屬於複數的範疇,然後者屬於實數的範圍。程序員

同時須要注意的是,NaN 只會出如今浮點類型中,而不會出如今 int 類型裏(固然 JS 並無這個概念)github

什麼意思?用你熟悉的任何支持 int 和 double 兩種類型的語言(好比 C)。在保證它不會偷偷作隱式類型轉換的狀況下,分別用 int 和 double 打印出 sqrt(-1), 你就能發現只有在 double 的類型下才能看到 NaN 出現,而 int 呢?編譯器甚至會給你一個 Warning。bash

那麼在浮點數下是如何表示一個 NaN 的呢?爲了方便,下面用單精度 float 來表示,請看下圖。 網絡

在 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 就能夠了。app

因此按上面的說法,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。 最後咱們獲得了: ui

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


兩種 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 吧

因此 Number.isNaN 的 polyfill 能夠怎麼實現呢?

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

就是這麼簡單 😎

參考文獻

相關文章
相關標籤/搜索