Skip to content

为什么在 JavaScript 中 NaN !== NaN?背后藏着 40 年的技术故事 #378

@mqyqingfeng

Description

@mqyqingfeng

1. 前言

初学 JavaScript 的时候,经常会遇到一些令人困惑的现象,比如:

console.log(NaN === NaN); // false
console.log(NaN !== NaN); // true

为什么一个值会不等于它自己呢?

今天,我们就来深入探究这个问题。

2. NaN 的本质:一个特殊的“数字”

NaN 其实是 Not a Number 的缩写,表示它不是一个数字。但 NaN 的类型却是 number

console.log(typeof NaN); // "number"

所以你可以把 NaN 理解为一个数字类型的特殊值。

当你尝试将非数字字符串转换为数字,或者进行无效的数学运算时,就会得到 NaN:

+"oops"; // NaN
0 / 0; // NaN

而当 NaN 出现在数学运算中时,它会导致所有运算结果都是 NaN:

console.log(NaN + 1); // NaN
console.log(NaN - 1); // NaN
console.log(Math.max(NaN, 5)); // NaN

3. 深入底层:IEEE 754 标准的故事

要理解 NaN !== NaN 的根源,我们需要回到 1985 年。

当时,IEEE 发布了 754 号标准——二进制浮点数算术标准

这个标准定义了浮点数的表示格式,包括一些特殊值:无穷大(Infinity)、负零(-0)和 NaN。

IEEE 754 标准规定,当指数部分为 0x7FF 而尾数部分非零时,这个值表示 NaN。

更重要的是,标准明确要求 NaN 不等于自身。

3.1. 为什么会这样设计呢?

这其实是一种深思熟虑的设计,而非错误。主要原因是:

  1. 提供错误检测机制:在早期没有 isNaN() 函数的编程环境中,x != x是检测 NaN 的唯一方法
  2. 逻辑一致性:NaN 代表“不是数字”,一个非数值确实不应该等于另一个非数值,这在逻辑上也是通畅的

3.2. 跨语言的一致性

因此 NaN !== NaN 的行为不仅存在于 JavaScript,而是贯穿所有遵循 IEEE 754 标准的编程语言:

以 Python 为例:

#Python

import math

nan = float('nan')
print(nan != nan)  # True
print(nan == nan)  # False
print(math.isnan(nan))  # True

以 C++ 为例:

//C++

#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, proper way)
    return 0;
}

以 Rust 为例:

//Rust

fn main() {
    let nan = f64::NAN;
    println!("{}", nan != nan);  // true
    println!("{}", nan == nan);  // false
    println!("{}", nan.is_nan());  // true (proper way)
}

3.3. 硬件级别的实现

有趣的是,NaN 的比较行为不是在 JavaScript 引擎层面实现的,而是直接由 CPU 硬件提供的支持。想一想也很合逻辑,我们想要对数字进行运算,CPU 也是在操作数字,所以在 CPU 中进行运算会是最快的!

当我们查看 JavaScript 引擎源码时,会发现它们依赖底层系统的标准库:

// Firefox
bool isNaN() const { return isDouble() && std::isnan(toDouble()); }

// V8
if (IsMinusZero(value)) return has_minus_zero();
if (std::isnan(value)) return has_nan();

那 CPU 是如何识别 NaN 的呢?

以 x86 架构的 CPU 为例,它会用专门的 “浮点寄存器(xmm0)” 处理浮点数运算,还会用一条叫 ucomisd 的指令比较两个浮点数 —— 如果比较的是 NaN,这条指令会设置一个 “奇偶标志位(PF=1)”,相当于给 CPU 发信号:“这是 NaN,不能正常比较!”

简单来说:当你写 NaN === NaN 时,底层 CPU 其实已经判断出 “这两个值特殊”,所以返回 false。

再直观一点,我们可以用 C 语言直接操作硬件寄存器,计算 “0.0/0.0”(这会生成 NaN):

#include <stdio.h>
#include <stdint.h>
int main() {
    double x = 0.0 / 0.0;
    // 直接读取 x 在内存中的二进制位
    uint64_t bits = *(uint64_t*)&x;
    printf("NaN 的十六进制表示:0x%016lx\n", bits);
    return 0;
}

运行结果会是 0xfff8000000000000—— 这正是 IEEE 754 标准规定的 NaN 存储格式,和 CPU 的处理逻辑完全对应。

4. JavaScript 不能没有 NaN

在 IEEE 754 标准之前,各硬件厂商有自己处理无效运算的方式。大多数情况下,像 0/0 这样的操作会直接导致程序崩溃

想象一下,如果没有 NaN:

// 我们需要对每个数学运算进行防御性检查
function safeDivide(a, b) {
  if (b === 0) {
    throw new Error("Division by zero!");
  }
  if (typeof a !== "number" || typeof b !== "number") {
    throw new Error("Arguments must be numbers!");
  }
  return a / b;
}

// 使用try-catch包围每个可能出错的运算
try {
  const result = safeDivide(10, 0);
} catch (e) {
  // 处理错误...
}

而有了 NaN,代码变得简洁而安全:

function divide(a, b) {
  return a / b; // 让硬件处理边界情况
}

const result = divide(10, 0); // Infinity
const invalidResult = 0 / 0; // NaN

if (Number.isNaN(invalidResult)) {
  // 在合适的地方统一处理错误
  console.log("检测到无效计算");
}

5. 实际开发中如何检测?

在日常开发中,我们应该如何使用 NaN 呢?

5.1. 使用 isNaN() 函数(不推荐)

console.log(isNaN(NaN)); // true
console.log(isNaN("hello")); // true - 注意:字符串会被先转换为数字

isNaN() 函数会先尝试将参数转换为数字,这可能导致意外的结果。

5.2. 使用 Number.isNaN()(推荐)

console.log(Number.isNaN(NaN)); // true
console.log(Number.isNaN("hello")); // false - 不会进行类型转换

ES6 引入的 Number.isNaN() 只会对真正的 NaN 值返回 true,是更安全的选择。

5.3. 使用 Object.is() 方法

console.log(Object.is(NaN, NaN)); // true

ES6 的 Object.is() 方法能正确识别 NaN,但它使用严格相等比较,适用于特殊场景。

6. 总结

NaN !== NaN 是 JavaScript 中一个看似奇怪但却设计合理的特性。它背后是 IEEE 754 标准的深思熟虑,目的是为浮点数运算提供一致且可靠的错误处理机制。

在实际开发中,记住以下几点:

  1. 始终使用Number.isNaN() 而不是 isNaN() 来检测 NaN 值
  2. 含有 NaN 的数学运算总会产生 NaN
  3. **利用这一特性****在代码中优雅地处理错误情况**
  4. 记住 NaN 是数字类型的特殊值,这在类型检查时很重要

7. 参考链接

  1. NaN, the not-a-number number that isn’t NaN
  2. Why NaN !== NaN in JavaScript (and the IEEE 754 story behind it)

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions