分析 `JavaScript` `NaN` 和 `typeof NaN` 的特殊性,以及 `IEEE 754` 双精度浮点数标准对 `JavaScript` 数字计算的影响。

各位晚上好,欢迎来到今晚的“JavaScript 奇葩说”。我是今晚的主讲人,江湖人称“代码老中医”,专治各种疑难杂症,尤其擅长解读 JavaScript 里那些让人挠头的怪现象。今天咱们就来聊聊 JavaScript 里一个非常特殊,而且经常让人掉坑里的东西:NaN

NaN:你不是一个数字,但你是数字类型的?!

首先,我们来认识一下 NaNNaN 的全称是 "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

正因为 NaNIEEE 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 数字计算的影响

除了 NaNIEEE 754 标准还对 JavaScript 的数字计算产生了很多其他的影响。

精度问题

由于 IEEE 754 使用有限的位数来表示数字,因此无法精确地表示所有的实数。这就导致了精度问题。

console.log(0.1 + 0.2); // 0.30000000000000004

你可能会觉得很奇怪,0.1 + 0.2 应该等于 0.3 才对,为什么 JavaScript 会输出 0.30000000000000004 呢?

这是因为 0.10.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^532^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_INTEGERNumber.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 奇葩说”就到这里。谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注