在查看本文以前,请先思考两个问题。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。bash
那么在浮点数下是如何表示一个 NaN 的呢?为了方便,下面用单精度 float 来表示,请看下图。 网络
其中 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 属于“脾气比较好”,比较“文静”的一种,你甚至能够直接定义它,并使用它。 好比咱们在 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;
}
复制代码
就是这么简单 😎