12k
All articles

JavaScript 中 NaN 的奇特行为

JavaScript 中的 NaN 遵循 IEEE 754 规范,可能引发静默错误,应使用 Number.isNaN 检测,并在 JSON 序列化前校验输入。

OpenReplay Team
OpenReplay Team
JavaScript 中 NaN 的奇特行为

你肯定遇到过这种情况:某个计算返回了 NaN,然后整个数据处理流程都输出了垃圾数据。更糟糕的是,本应捕获问题的比较操作却悄无声息地失败了,因为 NaN === NaN 返回 false。理解 JavaScript 中 NaN 的怪异行为不是可选项——这对于编写可靠的数值代码至关重要。

本文将解释 NaN 为什么会有这样的行为、它通常在哪里出现,以及如何使用现代方法正确检测它。

核心要点

  • NaN 是符合 IEEE 754 标准的有效数字类型,表示未定义或无法表示的数学结果。
  • NaN 永远不等于自身——使用 Number.isNaN()Object.is() 进行可靠检测。
  • 全局的 isNaN() 会先强制转换参数,导致误报;应避免使用。
  • JSON 序列化会静默地将 NaN 转换为 null,而 structuredClone() 会保留它。
  • 在边界处验证数值输入,防止 NaN 在计算中传播。

为什么 NaN 存在以及为什么 typeof 返回 “number”

NaN 代表 “Not a Number”(不是数字),但 typeof NaN 却返回 "number"。这不是 JavaScript 的 bug——而是有意为之的设计。

JavaScript 遵循的 IEEE 754 浮点数标准将 NaN 定义为数字类型中的一个特殊值。它表示未定义或无法表示的数学运算结果。可以把它看作一个占位符,表示”这个计算没有产生有效的数值结果”。

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

NaN 的存在是因为数值运算需要一种方式来表示失败,而不是抛出异常。在许多语言中,整数除以零会导致程序崩溃,但 0 / 0 返回 NaN 并让程序继续执行。

JavaScript 中 NaN 的常见来源

NaN 出现的频率可能超出你的预期:

// 解析失败
Number("hello")        // NaN
parseInt("abc")        // NaN

// 无效的数学运算
0 / 0                  // NaN
Math.sqrt(-1)          // NaN
Infinity - Infinity    // NaN

// 与 undefined 的运算
undefined * 5          // NaN

表单输入是常见的罪魁祸首。当 parseFloat(userInput) 遇到非数字文本时,NaN 会悄悄进入你的计算,并在后续的每个操作中传播。

NaN 比较规则:为什么 NaN !== NaN

这是让大多数开发者困惑的行为:

NaN === NaN  // false
NaN !== NaN  // true

IEEE 754 标准强制要求这样做。其理由是:如果两个操作都失败了,你不能假设它们的”失败”是等价的。Math.sqrt(-1)0 / 0 都产生 NaN,但它们代表不同的未定义结果。

这意味着你不能使用相等运算符来检测 NaN。

Number.isNaN vs isNaN:关键区别

JavaScript 提供了两个用于 NaN 检测的函数,但只有一个能可靠工作。

全局的 isNaN() 会先将参数强制转换为数字:

isNaN("hello")     // true (强制转换为 NaN,然后检查)
isNaN(undefined)   // true
isNaN({})          // true

这会产生误报。字符串 "hello" 本身不是 NaN——它是一个在强制转换时变成 NaN 的字符串。

Number.isNaN() 检查时不进行强制转换:

Number.isNaN("hello")     // false
Number.isNaN(undefined)   // false
Number.isNaN(NaN)         // true

始终使用 Number.isNaN() 进行准确检测。或者,Object.is(value, NaN) 也能正确工作:

Object.is(NaN, NaN)  // true

JSON 中的 NaN 行为:静默的数据丢失

当你序列化包含 NaN 的数据时,JSON.stringify() 会将其替换为 null:

JSON.stringify({ value: NaN })  // '{"value":null}'
JSON.stringify([1, NaN, 3])     // '[1,null,3]'

这是向 API 发送数值数据或将其存储在数据库时常见的 bug 来源。你的 NaN 值会静默消失,而你收到的是 null——这可能在下游引发不同的错误。

如果需要保留 NaN,请在序列化之前验证数值数据。

structuredClone 中的 NaN:现代克隆中的保留

与 JSON 不同,structuredClone() 会保留 NaN 值:

const original = { score: NaN }
const cloned = structuredClone(original)

Number.isNaN(cloned.score)  // true

这使得 structuredClone() 在深度复制可能包含无效数值结果的对象时对 NaN 是安全的。如果你要克隆可能包含 NaN 值的数据结构,优先使用 structuredClone() 而不是 JSON 往返转换。

NaN 传播:病毒式效应

一旦 NaN 进入计算,它会感染每个结果:

const result = 5 + NaN      // NaN
const final = result * 100  // NaN

这种传播是有意为之的——它防止你意外使用损坏的数据。但这意味着管道早期的单个 NaN 可能会使整个数据集失效。

在边界处验证输入:解析用户输入、接收 API 响应或从外部源读取数据时。

结论

一旦你理解了 IEEE 754 的设计,NaN 的奇怪行为就遵循了合乎逻辑的规则。使用 Number.isNaN()Object.is() 进行检测——永远不要使用相等运算符或全局的 isNaN()。记住 JSON 序列化会将 NaN 转换为 null,而 structuredClone() 会保留它。在入口点验证数值数据,在 NaN 通过计算传播之前捕获它。

常见问题

为什么 typeof NaN 返回 number 而不是类似 invalid 的值?

NaN 被 JavaScript 实现的 IEEE 754 浮点数标准定义为一个特殊的数值。它表示未定义数学运算的结果,同时保持在数字类型系统内。这种设计允许数值计算在运算失败时继续进行,而不抛出异常。

我可以使用相等运算符来检查值是否为 NaN 吗?

不可以。NaN 是 JavaScript 中唯一不等于自身的值。NaN === NaN 和 NaN == NaN 都返回 false。请使用 Number.isNaN() 或 Object.is(value, NaN) 进行可靠检测。

isNaN 和 Number.isNaN 有什么区别?

全局的 isNaN() 在检查之前会将参数强制转换为数字,对非数值(如字符串或 undefined)会产生误报。Number.isNaN() 不进行强制转换,仅当值确实是 NaN 时才返回 true。始终优先使用 Number.isNaN() 以获得准确结果。

如何防止 NaN 破坏我的计算?

在入口点验证所有数值输入,如表单字段、API 响应和外部数据源。在解析或接收数据后立即使用 Number.isNaN() 检查值。这可以阻止 NaN 通过后续操作传播并使结果失效。

Open-source session replay

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.