细读 JS | 详谈一下 NaN
一、NaN 的怪诞行为?
NaN 是 Not-A-Number 的简写,表示「不是一个数字」的意思,尽管如此,它却属于 Number 类型。
typeof NaN === 'number' // true
平常所写的 NaN
只是「全局变量」的一个「属性」而已,该属性的「初始值」为 NaN。为了便于区分,下文中高亮 NaN
表示全局变量的属性,NaN 表示属性值。
类似的还有 undefined
、Infinity
、globalThis
,它们都是全局对象属性。
前面故意提到 NaN
的初始值为 NaN,原因是在 ES3 时代,该属性是可以被覆盖的,也就是 writable 的(这点与 undefined
表现是一致的),自 ES5 起就不可被重新赋值了。
需要注意的是,NaN 是 JavaScript 中唯一一个不等于其本身的值。这样的话,全等比较结果为 false
是不是看起来合乎情理了?
NaN === NaN // false
利用这一特性,可以快速地写出一个判断某个值是否为 NaN 的方法:
const myIsNaN = x => x !== x myIsNaN(NaN) // true
小结:
- 虽然 NaN 表示一个不为数字的值,但它类型却属于 Number 类型。
- NaN 是 JavaScript 中唯一一个不等于其本身的值。
NaN === NaN
的比较结果为false
。
二、isNaN()、Number.isNaN()、Number.NaN 傻傻分不清?
先上几个菜尝尝鲜:
NaN == NaN // false NaN === NaN // false NaN === Number.NaN // false
isNaN(NaN) // true isNaN('NaN') // true isNaN('string') // true isNaN(undefined) // true isNaN({}) // true isNaN('11abc') // true isNaN(new Date().toString()) // true isNaN(null) // false isNaN(10) // false isNaN('10') // false isNaN('10.2') // false isNaN('') // false isNaN(' ') // false isNaN(new Date()) // false
Number.isNaN(NaN) // true Number.isNaN(Number.NaN) // true Number.isNaN(0 / 0) // true Number.isNaN('NaN') // false Number.isNaN('') // false Number.isNaN(' ') // false Number.isNaN(10) // false Number.isNaN(undefined) // false Number.isNaN(null) // false Number.isNaN({}) // false Number.isNaN(new Date()) // false Number.isNaN(new Date().toString()) // false
希望你没有晕,其实掌握内在原理就很简单了,顶多是看起来「反直觉」而已...
2.1 Number.NaN
它是内置对象 Number
提供的一个「静态属性」,其值就是 NaN,且「只读」。
从 ECMAScript 1st Edition #15.7.3.4 可以看到:
The value of Number.NaN is NaN.
This property shall have the attributes { DontEnum, DontDelete, ReadOnly }.
这大概就是与全局对象属性 NaN
的唯一区别吧。
2.2 isNaN()
它是全局对象的一个方法,用于判断一个值是否为 NaN。
从 ECMAScript 1st Edition #15.1.2.6 可以看到:
Applies ToNumber to its argument, then returns true if the result is NaN, and otherwise returns false.
从规范描述可知,它内部做了「类型转换」。
以 isNaN(x)
为例,它先将 x
转换为 Number 类型(即规范中的 ToNumber 抽象操作),然后再判断转换后的值是否为 NaN,若为 NaN 返回 true
,否则返回 false
。比如:
const str = 'string' isNaN(str) // true // 相当于 const transformedStr = Number(str) // NaN isNaN(transformedStr) // true
这样的特性有什么用呢,MDN 是这样介绍的,请看这里。
基于此,我们可以快速写出其 Polyfill 方法:
globalThis.myIsNaN = function (value) { const transformedValue = Number(vulue) return transformedValue != transformedValue }
若对数据类型转换不是太熟,可以阅读文章(隐式)数据类型转换详解。
2.3 Number.isNaN()
从命名上看,globalThis.isNaN()
它是反直觉的,它偷偷给我们做了一次类型转换。
可能正是因为这个原因,所以 ES6 标准中提供了一个全新的方法 Number.isNaN()
,其内部逻辑如下(可看):
- 若入参值不是 Number 类型,则返回
false
;- 若入参值为
NaN
则返回true
,否则返回false
。
与 isNaN()
不同的是,它不会对传入的值做类型转换。因此,可以快速写出其 Polyfill 方法:
Number.myIsNaN = function (value) { if (typeof value !== 'number') return false return value !== value }
都 2022 年了,都用 Number.isNaN()
来判断吧,其余的就交给 Babel 了。
2.4 小结
Number.NaN
静态属性的返回值正是 NaN 本身,该属性只读。- 在 ES5 及以上,可以认为
globalThis.NaN
等价于Number.NaN
,它们的值均为 NaN。isNaN()
比Number.isNaN()
多了一个Number()
过程,实际项目中若要判断一个值是否为 NaN,建议使用后者。
三、indexOf() 和 includes() 对 NaN 是如何判断的?
其实在另一篇文章《相等比较详解》已经介绍过了。
const arr = [NaN] arr.indexOf(NaN) // -1 arr.includes(NaN) // true
究其原因,是由于其内部使用了不同的比较算法。
这俩算法对 NaN 的处理如下(详见):
另外,全等比较也使用了 Number::equal (x, y) 算法:
两个 Number 类型的值的比较,其实都是围绕了这三个算法:Number::equal (x, y)、Number:: sameValue (x, y)、Number::sameValueZero (x, y),有兴趣可以看下。它们之中比较特别的值无非就是 +0
、-0
、NaN
。
四、其他
此前写过不少关于 JavaScript 数据类型的一些文章:
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论