本文主要参考并扩展自 Why NaN !== NaN in JavaScript (and the IEEE 754 story behind it),结合个人理解和补充说明,深入探讨 NaN 从语言层到硬件层的实现原理。
开篇
写 JavaScript 的时候,遇到过这样的情况:
js
> typeof NaN
'number'
> NaN === NaN
false
看到这里,脑子里冒出几个问号:
- NaN 明明是"不是数字",为啥
typeof显示是number? - 为什么 NaN 不等于它自己?这不是违背了数学中"任何值都等于它自己"的基本原则吗?
- NaN 参与运算总是返回 NaN,那它到底有什么用?
- 这个反常识的设计是 JavaScript 的 bug 还是有意为之?
有人说这是 JavaScript 的历史包袱,但深入研究后发现,NaN 的设计根本不是 JavaScript 层面的事------它从硬件层就已经实现了,而且是解决了一个更大问题的优雅方案。
NaN 究竟是什么?
一个永远返回自己的"数字"
试着对 NaN 做点运算:
js
> NaN + 1
NaN
> NaN - 1
NaN
> Math.max(NaN)
NaN
> Math.min(NaN)
NaN
无论加减还是求最大最小值,结果永远是 NaN。这看起来毫无意义,但这正是 NaN 的设计初衷------让错误在计算链中传播,而不是立即中断程序。
不只是 JavaScript 的问题
看看 Firefox 和 V8 引擎的源码:
js
// Firefox
bool isNaN() const {
return isDouble() && std::isnan(toDouble());
}
// V8
if (IsMinusZero(value)) return has_minus_zero();
if (std::isnan(value)) return has_nan();
浏览器引擎都调用了 C++ 标准库的 std::isnan 方法。这说明 NaN 不是 JavaScript 发明的,而是来自更底层的标准。
事实上,NaN 首次被标准化是在 1985 年,由 IEEE 754 浮点数标准定义。
从 JavaScript 到硬件层:NaN 的真实面目
用 C 语言验证
写个简单的 C 程序,看看 NaN 在底层是怎么表现的:
js
#include <math.h>
#include <stdint.h>
#include <stdio.h>
int main() {
double x = 0.0 / 0.0;
if (x != x) {
printf("NaN is not the same\n");
}
if (isnan(x)) {
printf("x is NaN\n");
}
uint64_t bits = *(uint64_t*)&x;
printf("NaN hex: 0x%016lx\n", bits);
return 0;
}
输出结果:
js
NaN is not the same
x is NaN
NaN hex: 0xfff8000000000000
跟 JavaScript 里的行为完全一样!
其他语言也是如此
Python:
py
import math
nan = float('nan')
print(nan != nan) # True
print(nan == nan) # False
print(math.isnan(nan)) # True
C++:
py
#include <iostream>
#include <cmath>
int main() {
double nan = NAN;
std::cout << (nan != nan) << std::endl; // 1 (true)
std::cout << (nan == nan) << std::endl; // 0 (false)
std::cout << std::isnan(nan) << std::endl; // 1 (true)
return 0;
}
Rust:
rust
fn main() {
let nan = f64::NAN;
println!("{}", nan != nan); // true
println!("{}", nan == nan); // false
println!("{}", nan.is_nan()); // true
}
这不是某种语言的设计缺陷,而是所有现代编程语言都遵循的统一标准。
深入汇编:NaN 的硬件实现
把刚才的 C 程序生成汇编代码,看看 CPU 是怎么处理 NaN 的:
ini
# =====================================
# double x = 0.0 / 0.0;
# =====================================
pxor xmm0, xmm0 # xmm0 = 0.0
divsd xmm0, xmm0 # xmm0 = 0.0 / 0.0 = NaN
movsd QWORD PTR -8[rbp], xmm0 # x = NaN
# =====================================
# if (x != x) {
# =====================================
movsd xmm0, QWORD PTR -8[rbp] # xmm0 = x
ucomisd xmm0, QWORD PTR -8[rbp] # compare x with x (sets PF=1 for NaN)
jnp .L2 # skip if NOT NaN (PF=0)
几个关键点:
- xmm0 寄存器 - 专门用于浮点数运算的 CPU 寄存器
- divsd 指令 - 执行浮点数除法,0.0/0.0 会产生 NaN
- ucomisd 指令 - 这是检测 NaN 的关键!
ucomisd:硬件级别的 NaN 检测
ucomisd 全称是 Unordered Compare Scalar Double-precision floating-point(无序标量双精度浮点数比较)。这条指令在 CPU 层面就能检测 NaN,会设置一个特殊的标志位(PF=1)。
这意味着什么?NaN 是在硬件层实现的,不是 JavaScript 抽象出来的概念。
为什么 NaN !== NaN?
历史原因:给程序员一个检测手段
在早期,很多编程语言还没有 isnan() 函数。工程师们需要一种方法来检测 NaN,于是设计了这个特性:
scss
if (x != x) {
// x 一定是 NaN
}
从逻辑上讲,这也说得通:一个"非值"不能等于另一个"非值" 。
不是 Bug,是精心设计
这是 IEEE 754 标准的有意设计,不是 JavaScript 的失误。所有遵循 IEEE 754 标准的语言都是这样实现的。
为什么 typeof NaN === "number"?
NaN 是 IEEE 754 数值系统的一部分,不是单独的类型。它是一个特殊的数值,用来表示数学运算错误。
在 JavaScript 中,number 类型的值都以双精度浮点数(double)表示,遵循 IEEE 754 标准。
整数 vs 浮点数的除零处理
整数除以零是明确的错误,会导致程序崩溃。但浮点数运算有很多会产生未定义结果的情况:
0.0 / 0.0→ NaN∞ - ∞→ NaN0 * ∞→ NaNsqrt(-1)→ NaN
在 IEEE 754 标准出现之前,每个硬件厂商对这些情况的处理都不一样,导致代码可移植性极差。
IEEE 754 标准:浮点数运算的统一规范
标准概览
- 发布时间: 1985 年
- 主要贡献者: William Kahan(加州大学伯克利分校)+ IEEE 委员会
- 定义内容: NaN、Infinity、非规格化数、舍入模式
关键决策
- NaN !== NaN - 比较时总是返回
false - NaN 的二进制表示 - 指数 =
0x7FF,尾数 ≠ 0 - Quiet NaN (qNaN) - 在运算中传播,不触发异常
- Signaling NaN (sNaN) - 第一次使用时触发异常
- NaN 传播规则 - 任何涉及 NaN 的运算结果都是 NaN
1994 年:奔腾 FDIV Bug
1994 年,Intel 奔腾处理器的浮点除法出现 bug,某些除法运算给出错误结果。虽然不是 NaN 的问题,但这个事件凸显了精确实现 IEEE 754 标准的重要性。
Intel 更换了数百万颗处理器,损失了 4.75 亿美元。
NaN:程序员的救星
没有 NaN 之前的世界
在 IEEE 754 标准之前(1985 年),每个硬件厂商都有自己的做法,通常意味着 0/0 这种运算会直接让程序崩溃。
想象一下:你坐在飞机上,控制系统的程序员没有预料到某个 0/0 运算,指令在 CPU 上执行,触发 Division Error,整个程序崩溃------飞机失控!
这要求开发者在每次运算前都做防御性编程。Intel 和其他厂商受够了不同架构上程序行为不一致的混乱局面。
为什么选择 NaN?
考虑几种可能的方案:
方案 A: Division Error → 程序崩溃(IEEE 754 之前)
- 程序意外终止(参见飞机例子)
- 每次运算前都要做防御性检查
方案 B: 返回 0
- 数学上不正确
- 掩盖了错误
- 后续计算会给出错误结果
方案 C: 返回 null 或错误码
- 每次运算后都要检查
- 中断数学计算链
- 结果类型不一致
方案 D: 特殊值 NaN(IEEE 754 的选择)
- 值在计算中传播
- 程序继续运行
- 可以在计算结束时检查结果
- 保持类型一致性(number)
对比:有 NaN 和没有 NaN 的代码
没有 NaN 的情况
js
function divide(a, b) {
// 检查类型
if (typeof a !== 'number' || typeof b !== 'number') {
throw new Error('Arguments must be numbers');
}
// 检查数字是否有效
if (!isFinite(a) || !isFinite(b)) {
throw new Error('Arguments must be finite');
}
// 检查除数
if (b === 0) {
throw new Error('Division by zero');
}
return a / b;
}
function calculate(expression) {
try {
const result = divide(10, 0);
return result;
} catch (e) {
console.error(e.message);
return null; // 该返回什么? null? undefined? 0?
}
}
每次运算都要写一堆检查代码,一旦出错就要处理异常,计算链被打断。
有 NaN 的情况
js
function divide(a, b) {
return a / b; // 硬件搞定一切!
}
function calculate(expression) {
return divide(10, 0);
}
const result = calculate("10 / 0");
console.log("Result:", result); // Infinity
const badResult = 0 / 0;
if (Number.isNaN(badResult)) {
console.log("Invalid calculation");
}
代码简洁多了,不需要在每一步都检查错误。硬件层面已经处理好了异常情况,错误会在计算链中传播,最后统一检查即可。
总结
研究完 NaN,我的理解是:
原理层面:
- NaN 在硬件层实现(通过
ucomisd等 CPU 指令) - 是 IEEE 754 标准的一部分(1985 年)
- 在运算中传播,允许在计算结束时检测错误
NaN !== NaN是有意设计,方便检测
实用层面:
- 避免了程序因浮点数运算错误而崩溃
- 保持了类型一致性(始终是
number) - 简化了错误处理逻辑
- 软肋:需要显式检查(使用
Number.isNaN())
使用建议:
- 检测 NaN :用
Number.isNaN(value),不要用value !== value(虽然也能用,但可读性差)
javascript
// ✅ 推荐
if (Number.isNaN(result)) {
console.log("计算出错");
}
// ❌ 不推荐(虽然可行)
if (result !== result) {
console.log("计算出错");
}
- 避免 NaN 传播:如果不希望 NaN 污染整个计算链,在关键步骤检查
javascript
function safeCalculate(a, b) {
const result = a / b;
return Number.isNaN(result) ? 0 : result; // 或其他默认值
}
- 理解 typeof 的结果 :
typeof NaN === "number"不是 bug,记住它是数值系统的一部分
写在最后
NaN 的设计是浮点数运算错误处理的优雅解决方案。它让程序在遇到异常数学运算时不会崩溃,同时保留了错误信息,允许程序员在合适的时机统一处理。
下次看到 NaN !== NaN 时,你会知道:
- 这不是 JavaScript 的 bug
- 这是 IEEE 754 标准在 1985 年就确定的设计
- 它在 CPU 硬件层就已经实现
- 这个"反常识"的行为实际上是深思熟虑的结果
从 JavaScript 的抽象层一路追踪到 CPU 指令,发现一个看似简单的语言特性背后,是几十年计算机工程的智慧结晶。这也提醒我们:遇到"反常识"的设计时,不妨多问几个为什么,往往能发现更深层的道理。
参考资料
- IEEE 754 - Wikipedia - IEEE 浮点数标准的历史和技术细节
- Why NaN !== NaN in JavaScript (and the IEEE 754 story behind it) - 本文的主要参考来源,深入讲解了 NaN 从 JavaScript 到硬件层的实现
- MDN - NaN - JavaScript 中 NaN 的官方文档
- MDN - Number.isNaN() - 检测 NaN 的正确方法