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

作者:冴羽日期:2025/11/10

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. 为什么会这样设计呢?

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

  1. 提供错误检测机制:在早期没有 isNaN() 函数的编程环境中,x != x是检测 NaN 的唯一方法
  2. 逻辑一致性: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 标准的深思熟虑,目的是为浮点数运算提供一致且可靠的错误处理机制。

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

  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)

为什么在 JavaScript 中 NaN !== NaN?背后藏着 40 年的技术故事》 是转载文章,点击查看原文


相关推荐


自定义instanceof运算符行为API: Symbol.hasInstance
桜吹雪2025/11/8

今天翻zod的源码,发现有个之前没见过的,应该也没听说的API:Symbol.hasInstance export /*@__NO_SIDE_EFFECTS__*/ function $constructor<T extends ZodTrait, D = T["_zod"]["def"]>( name: string, initializer: (inst: T, def: D) => void, params?: { Parent?: typeof Class } ): $con


Bash 的 md5sum 命令
hubenchang05152025/11/6

#Bash 的 md5sum 命令 md5sum [OPTION]... [FILE]... 功能 计算或校验 MD5 值。 类型 可执行文件(/usr/bin/md5sum),属于 coreutils。 参数 OPTION 选项: -b, --binary - 以二进制模式读取文件;类 UNIX 系统下始终是二进制模式 -c, --check - 从文件中读取 MD5 值进行校验 --tag- 生成 BSD 风格的输出 -t, --text - 以文本模式读取文件;类 UNIX 系统下不


虚拟机的未来:云计算与边缘计算的核心引擎(一)
jiushun_suanli2025/11/1

虚拟机定义与核心原理 虚拟机(VM)是指通过软件模拟实现的完整计算机系统,具有与物理计算机相同的功能。VM可以运行自己的操作系统和应用程序,就像独立的物理机器一样,但实际上是在共享的物理硬件资源上运行。 硬件虚拟化技术 硬件虚拟化是通过虚拟化层(hypervisor)在物理硬件和虚拟机之间建立抽象层,主要包括两种类型: 全虚拟化(Full Virtualization): 无需修改客户操作系统通过二进制翻译技术(如VMware的ESXi)或硬件辅助虚拟化(Intel VT-x/A


力扣热题100(前10道题目)
少年姜太公2025/10/30

前言 算法题几乎是面试必考的,许多同学一看到算法题就是一个头两个大,所以笔者这次准备把力扣热题100写成文章与jym一起学习,估计会分为10篇文章来写。在这个过程中会分享一些自己刷题的想法和思路,让大家能够轻松看懂,这些题目我会采用js来写,有看不懂js的同学可以看个思路然后换成自己熟悉的语言去写🔥 LeetCode 热题 HOT 100 在每道题目之前我都会把对应的题目链接贴出来,方便大家可以看完我的解法再去力扣上刷题,而且这些题目我会尽可能多种解法去写,大家可以参考一下。 160. 相交链


从原型到类:JavaScript面向对象编程的终极进化指南
良山有风来2025/10/27

你是不是也曾经被JavaScript的原型链绕得头晕眼花?每次看到__proto__和prototype就感觉在看天书?别担心,这几乎是每个前端开发者都会经历的阶段。 今天我要带你彻底搞懂JavaScript面向对象编程的进化之路。从令人困惑的原型到优雅的class语法,再到实际项目中的设计模式应用,读完本文,你不仅能理解JS面向对象的本质,还能写出更优雅、更易维护的代码。 原型时代:JavaScript的"上古时期" 在ES6之前,JavaScript面向对象编程全靠原型链。虽然语法看起来有点


Redis(81)Redis的缓存雪崩是什么?
Victor3562025/10/24

缓存雪崩的概念 缓存雪崩(Cache Avalanche)是指在某一时间段内,缓存中的大量数据同时过期,或者由于缓存服务器宕机导致大量请求直接打到数据库,导致数据库瞬时压力剧增,甚至可能导致数据库崩溃。 解决缓存雪崩的方法 为了解决缓存雪崩问题,可以采取以下几种策略: 缓存数据的过期时间设置为随机值:避免在同一时间大量缓存数据同时失效。 加锁或队列:在缓存失效时,通过机制控制对数据库的访问,避免大量请求同时打到数据库。 双写策略:更新缓存的同时也更新数据库,保证数据的一致性。 数据预热:在系统


KuiklyUI利用Kotlin Lambda函数实现声明式UI系统的深入分析
风冷2025/10/22

KuiklyUI利用Kotlin Lambda函数实现声明式UI系统的深入分析 KuiklyUI通过巧妙地利用Kotlin的lambda函数特性,构建了一套灵活、高效的声明式UI系统。本文将深入分析其实现机制和核心技术点。 一、Lambda函数在声明式UI中的核心应用 1. 接收器作用域函数的巧妙运用 KuiklyUI的声明式语法核心基于Kotlin的接收器作用域函数。在按钮组件ButtonView中,我们可以看到典型的实现: class ButtonView : ComposeView<But


面试问题—上家公司的离职原因
mapbar_front2025/10/21

面试结尾HR必问的问题,就是上一家公司的离职原因,作为多年的资深架构师,我做过多次终面,听到过千奇百怪的答案,有的真诚,有的官方,有的遮遮掩掩,有的情绪愤怒,这个问题是有正确答案的,今天就来和你分享一下。 1、真实的离职原因 其实离职无非就是两类原因,一类主动,一类被动。 主动,要么钱少,要么心累,但大多数情况都是钱少心又累。 被动,要么被行情拖累,要么末位淘汰,要么违纪被发现,这个问题只要不回答的稀碎,都不会影响你被录用。 2、避开下面两个错误答案 2.1、 破口大骂前公司前领导 有可能真的是


Redis(73)如何处理Redis分布式锁的死锁问题?
Victor3562025/10/20

处理Redis分布式锁的死锁问题,可以采取以下一些方法: 设置锁的自动过期时间: 使用EX和PX参数设置锁的超时时间,确保锁在持有过久后自动释放,避免死锁。 使用看门狗模式: 通过不断刷新锁的过期时间来维持锁的有效性,防止锁的持有者因为长时间操作而导致锁自动过期。 确保锁的唯一性和原子性: 使用具有唯一标识的锁值来确保释放锁时的准确性,避免因误删导致的死锁问题。 故障恢复机制: 在检测到锁持有者失效后,其他节点可以尝试获取锁,确保系统的高可用性。 下面是一个示


区块链技术的五大应用场景
终端域名2025/10/19

区块链技术凭借其去中心化、不可篡改和透明性等核心优势,已渗透至金融、供应链管理、医疗健康、知识产权保护及公共服务五大领域,成为重构信任机制与提升协作效率的关键技术。以下是对五大应用场景的详细阐述: 一、金融:重塑信任基石 跨境支付与清算 区块链通过分布式账本技术实现跨境交易的实时结算,显著降低传统SWIFT网络的中介成本和时间延迟。例如,Ripple、R3等区块链联盟已推动跨境汇款效率提升至分钟级,将跨国交易成本从每笔26美元降低至15美元。 数字货币与支付结算 央行数字货币(如中国

首页编辑器站点地图

Copyright © 2025 聚合阅读

License: CC BY-SA 4.0