Stories

Detail Return Return

為什麼在 JavaScript 中 NaN !== NaN?背後藏着 40 年的技術故事

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)
user avatar
0 users favorite the story!

Post Comments

Some HTML is okay.