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

1. 前言

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

javascript 复制代码
console.log(NaN === NaN); // false
console.log(NaN !== NaN); // true

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

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

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

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

javascript 复制代码
console.log(typeof NaN); // "number"

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

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

javascript 复制代码
+"oops"; // NaN
0 / 0; // NaN

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

javascript 复制代码
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 为例:


plain 复制代码
#Python

import math

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

以 C++ 为例:


plain 复制代码
//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 为例:

plain 复制代码
//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 引擎源码时,会发现它们依赖底层系统的标准库:

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):

plain 复制代码
#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:

javascript 复制代码
// 我们需要对每个数学运算进行防御性检查
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,代码变得简洁而安全:

javascript 复制代码
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() 函数(不推荐)

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

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

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

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

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

5.3. 使用 Object.is() 方法

javascript 复制代码
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)
相关推荐
久爱@勿忘2 小时前
vue下载项目内静态文件
前端·javascript·vue.js
前端炒粉2 小时前
21.搜索二维矩阵 II
前端·javascript·算法·矩阵
合作小小程序员小小店2 小时前
web网页开发,在线%台球俱乐部管理%系统,基于Idea,html,css,jQuery,jsp,java,ssm,mysql。
java·前端·jdk·intellij-idea·jquery·web
不爱吃糖的程序媛2 小时前
Electron 应用中的系统检测方案对比
前端·javascript·electron
泷羽Sec-静安2 小时前
Less-9 GET-Blind-Time based-Single Quotes
服务器·前端·数据库·sql·web安全·less
pe7er3 小时前
用高阶函数实现递归:从匿名函数到通用递归生成器
前端·javascript
IT古董3 小时前
全面理解 Corepack:Node.js 的包管理新时代
前端·node.js·corepack
Jonathan Star3 小时前
NestJS 是基于 Node.js 的渐进式后端框架,核心特点包括 **依赖注入、模块化架构、装饰器驱动、TypeScript 优先、与主流工具集成** 等
开发语言·javascript·node.js
学习3人组3 小时前
清晰地说明 NVM、NPM 和 NRM 在 Node.js 开发过程中的作用
前端·npm·node.js