JavaScript 的 NaN !== NaN 之谜:从 CPU 指令到 IEEE 754 标准的完整解密

本文主要参考并扩展自 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)

几个关键点:

  1. xmm0 寄存器 - 专门用于浮点数运算的 CPU 寄存器
  2. divsd 指令 - 执行浮点数除法,0.0/0.0 会产生 NaN
  3. 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
  • ∞ - ∞ → NaN
  • 0 * ∞ → NaN
  • sqrt(-1) → NaN

在 IEEE 754 标准出现之前,每个硬件厂商对这些情况的处理都不一样,导致代码可移植性极差。

IEEE 754 标准:浮点数运算的统一规范

标准概览

  • 发布时间: 1985 年
  • 主要贡献者: William Kahan(加州大学伯克利分校)+ IEEE 委员会
  • 定义内容: NaN、Infinity、非规格化数、舍入模式

关键决策

  1. NaN !== NaN - 比较时总是返回 false
  2. NaN 的二进制表示 - 指数 = 0x7FF,尾数 ≠ 0
  3. Quiet NaN (qNaN) - 在运算中传播,不触发异常
  4. Signaling NaN (sNaN) - 第一次使用时触发异常
  5. 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())

使用建议:

  1. 检测 NaN :用 Number.isNaN(value),不要用 value !== value(虽然也能用,但可读性差)
javascript 复制代码
// ✅ 推荐
if (Number.isNaN(result)) {
    console.log("计算出错");
}

// ❌ 不推荐(虽然可行)
if (result !== result) {
    console.log("计算出错");
}
  1. 避免 NaN 传播:如果不希望 NaN 污染整个计算链,在关键步骤检查
javascript 复制代码
function safeCalculate(a, b) {
    const result = a / b;
    return Number.isNaN(result) ? 0 : result;  // 或其他默认值
}
  1. 理解 typeof 的结果 :typeof NaN === "number" 不是 bug,记住它是数值系统的一部分

写在最后

NaN 的设计是浮点数运算错误处理的优雅解决方案。它让程序在遇到异常数学运算时不会崩溃,同时保留了错误信息,允许程序员在合适的时机统一处理。

下次看到 NaN !== NaN 时,你会知道:

  • 这不是 JavaScript 的 bug
  • 这是 IEEE 754 标准在 1985 年就确定的设计
  • 它在 CPU 硬件层就已经实现
  • 这个"反常识"的行为实际上是深思熟虑的结果

从 JavaScript 的抽象层一路追踪到 CPU 指令,发现一个看似简单的语言特性背后,是几十年计算机工程的智慧结晶。这也提醒我们:遇到"反常识"的设计时,不妨多问几个为什么,往往能发现更深层的道理。


参考资料

  1. IEEE 754 - Wikipedia - IEEE 浮点数标准的历史和技术细节
  2. Why NaN !== NaN in JavaScript (and the IEEE 754 story behind it) - 本文的主要参考来源,深入讲解了 NaN 从 JavaScript 到硬件层的实现
  3. MDN - NaN - JavaScript 中 NaN 的官方文档
  4. MDN - Number.isNaN() - 检测 NaN 的正确方法
相关推荐
群联云防护小杜2 小时前
国产化环境下 Web 应用如何满足等保 2.0?从 Nginx 配置到 AI 防护实战
运维·前端·nginx
醉方休2 小时前
Web3.js 全面解析
前端·javascript·electron
前端开发爱好者3 小时前
前端新玩具:Vike 发布!
前端·javascript
今天也是爱大大的一天吖3 小时前
vue2中的.native修饰符和$listeners组件属性
前端·javascript·vue.js
fxshy3 小时前
在 Vue 3 + Vite 项目中使用 Less 实现自适应布局:VW 和 VH 的应用
前端·javascript·less
奇舞精选3 小时前
AI时代的前端知识拾遗:前端事件循环机制详解(基于 WHATWG 最新规范)
前端·javascript
小月鸭3 小时前
理解预处理器(Sass/Less)
前端
AI3D_WebEngineer3 小时前
企业级业务平台项目设计、架构、业务全解之组件库篇
前端·javascript·vue
charlie1145141914 小时前
从零开始理解 CSS:让网页“活”起来的语言2
前端·css·笔记·学习·选择器·样式表·原生