在查看本文以前,請先思考兩個問題。html
typeof (1 / undefined)
是多少[1,2,NaN].indexOf(NaN)
輸出什麼若是你還不肯定這兩題的答案的話,請仔細閱讀本文。
這兩題的答案不會直接解釋,請從文章中尋找答案。git
咱們知道 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。網絡
那麼在浮點數下是如何表示一個 NaN 的呢?爲了方便,下面用單精度 float 來表示,請看下圖。
在 3b 狀況中,NaN 得知足:從左到右,以 1 開始,不關心第 1 位的值,第 2 位到第 9 位都是 1,剩下的位不全 爲 0。 關於 浮點數內部的組成,這裏不作具體的介紹,咱們只須要了解到浮點數分爲 3 個部分就能夠:app
其中 float 的指數位有 8 位,精度位有 32 - 1 - 8 = 23 位
double 的指數位有 11 位,精度位有 64 - 1 - 11 = 52 位
因此上面 NaN 的知足條件,能夠當作:精度位不全爲 0,指數位全 1 就能夠了。函數
因此按上面的說法,0x7f81111, 0x7fcccccc
等等這些都符合 NaN 的要求了。咱們能夠嘗試一下,本身寫一個函數,用來往 8 個字節的內存的前兩個字節寫入全 1. 也就是連續 16 個 1,這就符合 NaN 的定義了。看下面這段代碼:ui
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。
最後咱們獲得了:spa
看來創造一個 NaN 不是很難,對吧?
一樣的,爲了證實上面的圖的正確性,再看看 Infinity
的內部結構是否符合設計
若是再細分的話,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 的表示方式實在太多,僅僅在 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。
那麼實際狀況究竟是怎樣的呢?不一樣的系統會有不一樣的實現。
在 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
,有兩種可能:
而在 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; }
就是這麼簡單 😎