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. 為什麼會這樣設計呢?
這其實是一種深思熟慮的設計,而非錯誤。主要原因是:
- 提供錯誤檢測機制:在早期沒有
isNaN()函數的編程環境中,x != x是檢測 NaN 的唯一方法 - 邏輯一致性: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 標準的深思熟慮,目的是為浮點數運算提供一致且可靠的錯誤處理機制。
在實際開發中,記住以下幾點:
- 始終使用
Number.isNaN()而不是isNaN()來檢測 NaN 值 - 含有 NaN 的數學運算總會產生 NaN
- 利用這一特性**在代碼中優雅地處理錯誤情況**
- 記住 NaN 是數字類型的特殊值,這在類型檢查時很重要
7. 參考鏈接
- NaN, the not-a-number number that isn’t NaN
- Why NaN !== NaN in JavaScript (and the IEEE 754 story behind it)