toFrankie / blog

种一棵树,最好的时间是十年前。其次,是现在。
21 stars 1 forks source link

细读 JS | 详谈一下 NaN #291

Open toFrankie opened 1 year ago

toFrankie commented 1 year ago

Freepik

NaN 的怪诞行为?

NaN 是 Not-A-Number 的简写,表示「不是一个数字」的意思。

尽管如此,它却是 Number 类型。

typeof NaN === 'number' // true

平常所写的 NaN 只是「全局变量」的一个「属性」而已,该属性的「初始值」为 NaN。

为了便于区分,下文中高亮 NaN 表示全局变量的属性,NaN 表示属性值。

类似的还有 undefinedInfinityglobalThis,它们都是全局对象属性。

前面故意提到 NaN 的初始值为 NaN,原因是在 ES3 时代,该属性是可以被覆盖的,也就是 writable 的(这点与 undefined 表现是一致的),自 ES5 起就不可被重新赋值了。

The value of NaN is NaN.

需要注意的是,NaN 是 JavaScript 中唯一一个不等于其本身的值。这样的话,全等比较结果为 false 是不是看起来合乎情理了?

NaN === NaN // false

利用这一特性,可以快速地写出一个判断某个值是否为 NaN 的方法:

const myIsNaN = x => x !== x

myIsNaN(NaN) // true

小结:

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

希望你没有晕,其实掌握内在原理就很简单了,最多是「反直觉」而已...

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 的唯一区别吧。

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
}

若对数据类型转换不是太熟,可以阅读文章 👉(隐式)数据类型转换详解

Number.isNaN()

从命名上看,globalThis.isNaN() 它是反直觉的,它偷偷给我们做了一次类型转换。

可能正是因为这个原因,所以 ES6 标准中提供了一个全新的方法 Number.isNaN(),其内部逻辑如下(可看):

  1. 若入参值不是 Number 类型,则返回 false
  2. 若入参值为 NaN 则返回 true,否则返回 false

isNaN() 不同的是,它不会对传入的值做类型转换。因此,可以快速写出其 Polyfill 方法:

Number.myIsNaN = function (value) {
  if (typeof value !== 'number') return false
  return value !== value
}

都 2022 年了,都用 Number.isNaN() 来判断吧,其余的就交给 Babel 了。

小结

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-0NaN

其他

此前写过不少关于 JavaScript 数据类型的一些文章:

The end.