1. 前言
初学 JavaScript 的时候,经常会遇到一些令人困惑的现象,比如:
1console.log(NaN === NaN); // false 2console.log(NaN !== NaN); // true 3
为什么一个值会不等于它自己呢?
今天,我们就来深入探究这个问题。
2. NaN 的本质:一个特殊的“数字”
NaN 其实是 Not a Number 的缩写,表示它不是一个数字。但 NaN 的类型却是 number
1console.log(typeof NaN); // "number" 2
所以你可以把 NaN 理解为一个数字类型的特殊值。
当你尝试将非数字字符串转换为数字,或者进行无效的数学运算时,就会得到 NaN:
1+"oops"; // NaN 20 / 0; // NaN 3
而当 NaN 出现在数学运算中时,它会导致所有运算结果都是 NaN:
1console.log(NaN + 1); // NaN 2console.log(NaN - 1); // NaN 3console.log(Math.max(NaN, 5)); // NaN 4
3. 深入底层:IEEE 754 标准的故事
要理解 NaN !== NaN 的根源,我们需要回到 1985 年。
当时,IEEE 发布了 754 号标准——二进制浮点数算术标准。
这个标准定义了浮点数的表示格式,包括一些特殊值:无穷大(Infinity)、负零(-0)和 NaN。
IEEE 754 标准规定,当指数部分为 0x7FF 而尾数部分非零时,这个值表示 NaN。
更重要的是,标准明确要求 NaN 不等于自身。
3.1. 为什么会这样设计呢?
这其实是一种深思熟虑的设计,而非错误。主要原因是:
- 提供错误检测机制:在早期没有
isNaN()函数的编程环境中,x != x是检测 NaN 的唯一方法 - 逻辑一致性:NaN 代表“不是数字”,一个非数值确实不应该等于另一个非数值,这在逻辑上也是通畅的
3.2. 跨语言的一致性
因此 NaN !== NaN 的行为不仅存在于 JavaScript,而是贯穿所有遵循 IEEE 754 标准的编程语言:
以 Python 为例:
1#Python 2 3import math 4 5nan = float('nan') 6print(nan != nan) # True 7print(nan == nan) # False 8print(math.isnan(nan)) # True 9
以 C++ 为例:
1//C++ 2 3#include <iostream> 4#include <cmath> 5 6int main() { 7 double nan = NAN; 8 std::cout << (nan != nan) << std::endl; // 1 (true) 9 std::cout << (nan == nan) << std::endl; // 0 (false) 10 std::cout << std::isnan(nan) << std::endl; // 1 (true, proper way) 11 return 0; 12} 13
以 Rust 为例:
1//Rust 2 3fn main() { 4 let nan = f64::NAN; 5 println!("{}", nan != nan); // true 6 println!("{}", nan == nan); // false 7 println!("{}", nan.is_nan()); // true (proper way) 8} 9
3.3. 硬件级别的实现
有趣的是,NaN 的比较行为不是在 JavaScript 引擎层面实现的,而是直接由 CPU 硬件提供的支持。想一想也很合逻辑,我们想要对数字进行运算,CPU 也是在操作数字,所以在 CPU 中进行运算会是最快的!
当我们查看 JavaScript 引擎源码时,会发现它们依赖底层系统的标准库:
1// Firefox 2bool isNaN() const { return isDouble() && std::isnan(toDouble()); } 3 4// V8 5if (IsMinusZero(value)) return has_minus_zero(); 6if (std::isnan(value)) return has_nan(); 7
那 CPU 是如何识别 NaN 的呢?
以 x86 架构的 CPU 为例,它会用专门的 “浮点寄存器(xmm0)” 处理浮点数运算,还会用一条叫 ucomisd 的指令比较两个浮点数 —— 如果比较的是 NaN,这条指令会设置一个 “奇偶标志位(PF=1)”,相当于给 CPU 发信号:“这是 NaN,不能正常比较!”
简单来说:当你写 NaN === NaN 时,底层 CPU 其实已经判断出 “这两个值特殊”,所以返回 false。
再直观一点,我们可以用 C 语言直接操作硬件寄存器,计算 “0.0/0.0”(这会生成 NaN):
1#include <stdio.h> 2#include <stdint.h> 3int main() { 4 double x = 0.0 / 0.0; 5 // 直接读取 x 在内存中的二进制位 6 uint64_t bits = *(uint64_t*)&x; 7 printf("NaN 的十六进制表示:0x%016lx\n", bits); 8 return 0; 9} 10
运行结果会是 0xfff8000000000000—— 这正是 IEEE 754 标准规定的 NaN 存储格式,和 CPU 的处理逻辑完全对应。
4. JavaScript 不能没有 NaN
在 IEEE 754 标准之前,各硬件厂商有自己处理无效运算的方式。大多数情况下,像 0/0 这样的操作会直接导致程序崩溃。
想象一下,如果没有 NaN:
1// 我们需要对每个数学运算进行防御性检查 2function safeDivide(a, b) { 3 if (b === 0) { 4 throw new Error("Division by zero!"); 5 } 6 if (typeof a !== "number" || typeof b !== "number") { 7 throw new Error("Arguments must be numbers!"); 8 } 9 return a / b; 10} 11 12// 使用try-catch包围每个可能出错的运算 13try { 14 const result = safeDivide(10, 0); 15} catch (e) { 16 // 处理错误... 17} 18
而有了 NaN,代码变得简洁而安全:
1function divide(a, b) { 2 return a / b; // 让硬件处理边界情况 3} 4 5const result = divide(10, 0); // Infinity 6const invalidResult = 0 / 0; // NaN 7 8if (Number.isNaN(invalidResult)) { 9 // 在合适的地方统一处理错误 10 console.log("检测到无效计算"); 11} 12
5. 实际开发中如何检测?
在日常开发中,我们应该如何使用 NaN 呢?
5.1. 使用 isNaN() 函数(不推荐)
1console.log(isNaN(NaN)); // true 2console.log(isNaN("hello")); // true - 注意:字符串会被先转换为数字 3
isNaN() 函数会先尝试将参数转换为数字,这可能导致意外的结果。
5.2. 使用 Number.isNaN()(推荐)
1console.log(Number.isNaN(NaN)); // true 2console.log(Number.isNaN("hello")); // false - 不会进行类型转换 3
ES6 引入的 Number.isNaN() 只会对真正的 NaN 值返回 true,是更安全的选择。
5.3. 使用 Object.is() 方法
1console.log(Object.is(NaN, NaN)); // true 2
ES6 的 Object.is() 方法能正确识别 NaN,但它使用严格相等比较,适用于特殊场景。
6. 总结
NaN !== NaN 是 JavaScript 中一个看似奇怪但却设计合理的特性。它背后是 IEEE 754 标准的深思熟虑,目的是为浮点数运算提供一致且可靠的错误处理机制。
在实际开发中,记住以下几点:
- 始终使用
Number.isNaN()而不是isNaN()来检测 NaN 值 - 含有 NaN 的数学运算总会产生 NaN
- 利用这一特性**在代码中优雅地处理错误情况**
- 记住 NaN 是数字类型的特殊值,这在类型检查时很重要
7. 参考链接
- NaN, the not-a-number number that isn’t NaN
- Why NaN !== NaN in JavaScript (and the IEEE 754 story behind it)
《为什么在 JavaScript 中 NaN !== NaN?背后藏着 40 年的技术故事》 是转载文章,点击查看原文。
