各位晚上好,欢迎来到今晚的“JavaScript 奇葩说”。我是今晚的主讲人,江湖人称“代码老中医”,专治各种疑难杂症,尤其擅长解读 JavaScript 里那些让人挠头的怪现象。今天咱们就来聊聊 JavaScript 里一个非常特殊,而且经常让人掉坑里的东西:NaN
。
NaN:你不是一个数字,但你是数字类型的?!
首先,我们来认识一下 NaN
。NaN
的全称是 "Not a Number",意思是不是一个数字。
console.log(0 / 0); // NaN
console.log(Math.sqrt(-1)); // NaN
console.log(parseInt("hello")); // NaN
console.log(Number("abc")); // NaN
上面的例子中,这些运算的结果都不是一个有效的数字,所以返回了 NaN
。这很好理解,对吧?
但是!重点来了!
console.log(typeof NaN); // "number"
没错,你没看错!NaN
居然是 number
类型!这就像你跟别人说:“我不是人类”,然后别人问你:“那你是什么?”,你回答:“我是个人。” 听起来是不是有点矛盾?
这就是 NaN
最让人困惑的地方。它明明 "Not a Number",却又属于 number
类型。这就像一个悖论,让人感觉 JavaScript 在跟你开玩笑。
为什么会这样?这就要涉及到 IEEE 754
双精度浮点数标准了。
IEEE 754:浮点数的幕后推手
JavaScript 里的数字都是以 IEEE 754
双精度浮点数格式存储的。这个标准定义了如何用二进制来表示数字,包括整数、小数、正数、负数、以及一些特殊的值,比如正无穷大、负无穷大,当然也包括我们的主角 NaN
。
简单来说,IEEE 754
就像一个巨大的编码表,它定义了各种数字对应的二进制编码。而 NaN
也是这个编码表里的一个合法成员。
具体来说,IEEE 754
双精度浮点数使用 64 位来表示一个数字,这 64 位分为三个部分:
- 符号位 (Sign): 1 位,表示正负号 (0 表示正数,1 表示负数)
- 指数位 (Exponent): 11 位,用来表示指数
- 尾数位 (Mantissa/Significand): 52 位,用来表示小数部分
当指数位全部为 1,尾数位不为 0 时,就表示 NaN
。也就是说,NaN
其实有很多种不同的二进制表示形式,只要满足这个条件,都是 NaN
。
正因为 NaN
是 IEEE 754
标准里定义的一个合法值,所以 typeof NaN
才会返回 "number"
。
这就好比说,你定义了一个 "水果" 类,然后你创建了一个 "烂苹果" 对象,虽然 "烂苹果" 已经不能吃了,但它仍然属于 "水果" 类。
NaN 的特殊性:谁也不等于,除了…
除了类型上的特殊性,NaN
在比较运算上也表现得非常“有个性”。
console.log(NaN == NaN); // false
console.log(NaN === NaN); // false
console.log(NaN != NaN); // true
console.log(NaN !== NaN); // true
NaN
不等于任何值,包括它自己!这简直就是数字界的“孤僻症患者”。
这种特性会导致一些非常隐蔽的 bug。比如,你可能想用 ==
或 ===
来判断一个变量是否是 NaN
,但这样做永远都会返回 false
。
那应该怎么判断一个值是否是 NaN
呢?
isNaN()
函数:曾经的坑货
JavaScript 提供了一个全局函数 isNaN()
来判断一个值是否是 NaN
。但是,这个函数也存在一些问题,用起来需要格外小心。
console.log(isNaN(NaN)); // true
console.log(isNaN("hello")); // true
console.log(isNaN("123")); // false
console.log(isNaN(123)); // false
console.log(isNaN({})); // true
console.log(isNaN(null)); // false
console.log(isNaN(undefined)); // true
isNaN()
函数的判断逻辑是:首先尝试将参数转换为数字,如果转换失败,则返回 true
,否则返回 false
。
也就是说,isNaN()
函数实际上是判断一个值是否“不是一个数字”,而不是判断它是否是 NaN
。
这就导致了一些意想不到的结果。比如,isNaN("hello")
返回 true
,因为 "hello"
无法转换为数字。而 isNaN("123")
返回 false
,因为 "123"
可以转换为数字。
更让人迷惑的是,isNaN({})
和 isNaN(undefined)
也返回 true
,因为它们都无法转换为数字。
所以,isNaN()
函数并不是一个可靠的 NaN
检测工具。它很容易误判,导致 bug 的产生。
Number.isNaN()
函数:官方推荐的正确姿势
为了解决 isNaN()
函数的缺陷,ES6 引入了一个新的函数:Number.isNaN()
。
console.log(Number.isNaN(NaN)); // true
console.log(Number.isNaN("hello")); // false
console.log(Number.isNaN("123")); // false
console.log(Number.isNaN(123)); // false
console.log(Number.isNaN({})); // false
console.log(Number.isNaN(null)); // false
console.log(Number.isNaN(undefined)); // false
Number.isNaN()
函数的判断逻辑是:只有当参数的值严格等于 NaN
时,才返回 true
,否则返回 false
。它不会进行类型转换,也不会产生误判。
所以,Number.isNaN()
才是官方推荐的 NaN
检测工具。
我们可以简单地总结一下 isNaN()
和 Number.isNaN()
的区别:
函数 | 判断逻辑 | 是否进行类型转换 | 是否容易误判 |
---|---|---|---|
isNaN() |
判断一个值是否“不是一个数字” | 是 | 是 |
Number.isNaN() |
判断一个值是否严格等于 NaN |
否 | 否 |
IEEE 754 对 JavaScript 数字计算的影响
除了 NaN
,IEEE 754
标准还对 JavaScript 的数字计算产生了很多其他的影响。
精度问题
由于 IEEE 754
使用有限的位数来表示数字,因此无法精确地表示所有的实数。这就导致了精度问题。
console.log(0.1 + 0.2); // 0.30000000000000004
你可能会觉得很奇怪,0.1 + 0.2
应该等于 0.3
才对,为什么 JavaScript 会输出 0.30000000000000004
呢?
这是因为 0.1
和 0.2
这两个小数在 IEEE 754
标准下无法精确地表示,只能用近似值来代替。当这两个近似值相加时,得到的结果也是一个近似值,而不是精确的 0.3
。
这种精度问题在金融计算等对精度要求很高的场景下,可能会导致严重的错误。
为了解决这个问题,我们可以使用一些技巧,比如将小数转换为整数进行计算,或者使用专门的库来处理高精度计算。
// 将小数转换为整数进行计算
console.log((0.1 * 10 + 0.2 * 10) / 10); // 0.3
// 使用专门的库来处理高精度计算
// (需要引入 BigNumber.js 或类似的库)
// const x = new BigNumber(0.1);
// const y = new BigNumber(0.2);
// console.log(x.plus(y).toNumber()); // 0.3
安全整数范围
IEEE 754
双精度浮点数可以精确表示的整数范围是 -2^53
到 2^53
之间(包含边界值)。超出这个范围的整数,可能会出现精度丢失。
console.log(Math.pow(2, 53)); // 9007199254740992
console.log(Math.pow(2, 53) + 1); // 9007199254740992
console.log(Math.pow(2, 53) + 2); // 9007199254740994
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991
console.log(Number.MIN_SAFE_INTEGER); // -9007199254740991
可以看到,Math.pow(2, 53) + 1
的结果和 Math.pow(2, 53)
的结果是一样的,这就是精度丢失。
JavaScript 提供了一个常量 Number.MAX_SAFE_INTEGER
和 Number.MIN_SAFE_INTEGER
来表示最大安全整数和最小安全整数。
在进行整数计算时,应该尽量避免超出这个安全范围,否则可能会导致精度问题。如果需要处理超出安全范围的整数,可以使用 BigInt
类型。
BigInt
类型:安全整数的新选择
ES2020 引入了 BigInt
类型,用来表示任意精度的整数。
const bigInt = 9007199254740992n; // 注意后面的 "n"
console.log(bigInt + 1n); // 9007199254740993n
console.log(typeof bigInt); // "bigint"
BigInt
类型可以安全地表示任意大小的整数,不会出现精度丢失的问题。
需要注意的是,BigInt
类型和 number
类型是不同的类型,它们之间的运算需要进行类型转换。
// console.log(bigInt + 1); // 报错:Cannot mix BigInt and other types
console.log(bigInt + BigInt(1)); // 正确:9007199254740993n
console.log(Number(bigInt) + 1); // 正确:9007199254740992
总结:与数字共舞,小心陷阱
今天我们深入探讨了 JavaScript 里 NaN
的特殊性,以及 IEEE 754
标准对 JavaScript 数字计算的影响。
我们学习了:
NaN
的概念和类型isNaN()
和Number.isNaN()
的区别IEEE 754
标准对精度和安全整数范围的影响BigInt
类型的用法
希望通过今天的分享,大家能够对 JavaScript 的数字计算有更深入的了解,避免掉入 NaN
和精度问题的陷阱。
记住,与数字共舞,也要小心陷阱!
好了,今天的“JavaScript 奇葩说”就到这里。谢谢大家!