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のバグではなく、設計によるものです。
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はこれを義務付けています。その理由は:2つの演算が両方とも失敗した場合、それらの「失敗」が等価であると仮定することはできません。Math.sqrt(-1)と0 / 0は両方ともNaNを生成しますが、異なる未定義の結果を表しています。
つまり、等価演算子を使ってNaNを検出することはできません。
Number.isNaN vs isNaN:決定的な違い
JavaScriptはNaN検出のために2つの関数を提供していますが、信頼できるのは1つだけです。
グローバルな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
Discover how at OpenReplay.com.
JSONのNaN挙動:静かなデータ損失
NaNを含むデータをシリアライズすると、JSON.stringify()はそれをnullに置き換えます:
JSON.stringify({ value: NaN }) // '{"value":null}'
JSON.stringify([1, NaN, 3]) // '[1,null,3]'
これは、数値データをAPIに送信したり、データベースに保存したりする際のバグの一般的な原因です。NaN値は静かに消失し、nullを受け取ることになります。これは下流で異なるエラーを引き起こす可能性があります。
NaNの保持が重要な場合は、シリアライゼーション前に数値データを検証してください。
structuredCloneのNaN:最新のクローニングにおける保持
JSONとは異なり、structuredClone()はNaN値を保持します:
const original = { score: NaN }
const cloned = structuredClone(original)
Number.isNaN(cloned.score) // true
これにより、structuredClone()は無効な数値結果を含む可能性のあるオブジェクトのディープコピーにおいてNaN安全になります。潜在的なNaN値を持つデータ構造をクローンする場合は、JSONのラウンドトリップよりもstructuredClone()を優先してください。
NaNの伝播:ウイルス効果
NaNが計算に入ると、すべての結果に感染します:
const result = 5 + NaN // NaN
const final = result * 100 // NaN
この伝播は意図的なものです。破損したデータを誤って使用するのを防ぎます。しかし、パイプラインの早い段階での1つのNaNがデータセット全体を無効にする可能性があることを意味します。
境界で入力を検証してください:ユーザー入力のパース時、APIレスポンスの受信時、または外部ソースからの読み取り時です。
まとめ
NaNの奇妙な挙動は、IEEE 754の設計を理解すれば論理的なルールに従っています。検出にはNumber.isNaN()またはObject.is()を使用してください。等価演算子やグローバルなisNaN()は決して使用しないでください。JSONシリアライゼーションはNaNをnullに変換しますが、structuredClone()はそれを保持することを覚えておいてください。NaNが計算全体に伝播する前に捕捉するため、エントリーポイントで数値データを検証してください。
よくある質問
NaNはJavaScriptが実装するIEEE 754浮動小数点標準によって特別な数値として定義されています。これは数値型システム内に留まりながら、未定義の数学演算の結果を表します。この設計により、演算が失敗したときに例外をスローすることなく数値計算を継続できます。
いいえ。NaNはJavaScriptで自分自身と等しくない唯一の値です。NaN === NaNとNaN == NaNの両方がfalseを返します。信頼性の高い検出にはNumber.isNaN()またはObject.is(value, NaN)を使用してください。
グローバルなisNaN()はチェックする前に引数を数値に型強制するため、文字列やundefinedなどの非数値に対して偽陽性を引き起こします。Number.isNaN()は型強制を行わず、値が正確にNaNの場合のみtrueを返します。正確な結果を得るには常にNumber.isNaN()を優先してください。
フォームフィールド、APIレスポンス、外部データソースなどのエントリーポイントですべての数値入力を検証してください。パースまたはデータ受信の直後にNumber.isNaN()で値をチェックしてください。これにより、NaNが後続の演算に伝播して結果を無効にするのを防ぎます。
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.
Check our GitHub repo and join the thousands of developers in our community.