フロントエンドの現場で泥臭いバグと戦っていると、時々「お前、なんでそんな嘘をつくんだ…」と言いたくなるようなJavaScriptの挙動に遭遇する。
特に、データグリッドのソート機能や、入力値のバリデーション、あるいは無限スクロールの閾値判定などで、比較演算子(`<` や `>`)を使った瞬間にアプリケーションが沈黙する。原因を究明していくと、大抵犯人は「暗黙の型変換」と、JavaScript界の厄介者である `NaN`(Not-a-Number) だ。
今日は、中級からもう一歩抜け出すために、この比較演算子が裏側で何をやっているのか、そのブラックボックスの蓋を開けてみよう。
—
1. 比較演算子 `` の裏側の挙動(仕様の裏側)
JavaScriptで数値以外の型、例えば文字列やオブジェクトが比較演算子に渡されたとき、エンジン(V8など)は内部で厳格なアルゴリズムを回している。ECMAScript仕様書の言葉を借りるなら、これは Abstract Relational Comparison(抽象的関係比較) と呼ばれるプロセスだ。
大まかな流れはこうだ:
1. プリミティブ値への変換(ToPrimitive):
オペランドがオブジェクトであれば、まずはプリミティブ値(文字列や数値)に強制変換される。`valueOf()` が呼ばれ、ダメなら `toString()` が呼ばれる。
2. 数値への強制変換(ToNumber):
双方がプリミティブ値になったら、「どちらか一方が文字列であれば、両方を数値に変換する」 というルールが発動する(※両方が文字列の場合は、文字コードの辞書順比較になるので注意が必要だ)。
3. 数値同士の大小比較:
綺麗に数値に揃えられたところで、ようやく人間が期待する大小比較が行われる。
口で言うより、コードで見たほうが早いだろう。以下の挙動を見てほしい。
// 実務でやりがちな「文字列同士」の比較の罠
console.log(‘2’ < '10'); // ❌ false になる!
// 解説: 両方とも文字列の場合、JavaScriptは辞書順比較を行う。
// 文字コードの順序で '2' は '1' よりも後なので、'2' < '10' は false('1' が先に来るため)。
// 片方に数値、あるいは数値に変換されるオペが混ざると…
console.log('2' < 10); // ✅ true
// 解説: 一方が数値(10)、一方が文字列('2')なので、'2' が数値の 2 に変換され、2 < 10 となり true。
「文字コードの辞書順」と「数値としての大小」が、文字列の比較ではごっちゃになりやすい。APIから返ってきたIDが文字列の `"10"` で、それをソートしようとして見事にハマった経験、君たちもないか? 私は何度もあって、そのたびに夜食のカップラーメンをすする羽目になった。
---
2. 破壊神 `NaN` が混ざったときの絶望的なルール
さて、ここからが本題だ。JavaScriptにおける `NaN` は、文字通り「数ではない」ことを表すが、こいつの厄介なところは 「型としては `number` なのに、どの数値とも等しくなく、大小比較においても全否定をかましてくる」 という点だ。
仕様(ECMAScript)では、比較演算子に `NaN` が絡んだ場合のルールが冷酷に定められている。
> 「オペランドのどちらか(あるいは両方)が `ToNumber` の結果として `NaN` に評価された場合、関係比較の結果は必ず `false` になる」
これが何を意味するか、具体例を見てみよう。
console.log(NaN < 3); // false
console.log(NaN > 3); // false
console.log(NaN == 3); // false
console.log(NaN === 3);// false
ここまでは「まあ `NaN` だしな」と納得できる。問題は、「これを知らずにバリデーションを書いたとき」 だ。
// ユーザーが入力した年齢をチェックする関数(悪い例)
function isTooYoung(ageInput) {
// 「18未満なら若すぎる」と判定したい
if (ageInput < 18) {
return true;
}
return false;
}
console.log(isTooYoung(17)); // true (17 < 18)
console.log(isTooYoung(20)); // false (20 < 18)
console.log(isTooYoung('hoge')); // ❌ false が返ってくる!
最後の `'hoge'` を渡したケースを追ってみよう。
1. `'hoge'` は比較演算子によって `ToNumber('hoge')` にかけられる。
2. 数字に変換できないので、結果は `NaN` になる。
3. `NaN < 18` の評価は、前述のルールにより `false` になる。
4. 結果として、「不正な文字列が入力されたのに、`isTooYoung` は `false`(若すぎない=安全)」と判定してしまう。
これはセキュリティやビジネスロジックにおいて致命的なバグになり得る。不正な入力値が「エラー」ではなく「スルー」されてしまうからだ。
—
3. 実務で使える!安全な比較と型ガードのベストプラクティス
じゃあ、現場のエンジニアとしてどう立ち回るべきか?答えはシンプルだ。「暗黙の型変換に頼らず、比較の前に必ず明示的な型チェックと `NaN` の排除を行え」。
特にモダンなフロントエンドでは、TypeScriptを使っていても、APIのレスポンスや `FormData` から取得した値は `any` や `unknown`、あるいはガバガバな文字列であることがある。
以下に、実務のコードベースでそのまま使える堅牢なユーティリティ関数のパターンを置いておく。
/
- 安全に数値の大小比較を行うためのヘルパー関数
- @param {unknown} value – 比較対象の値(ユーザー入力やAPIレスポンスなど)
- @param {number} threshold – 閾値
- @returns {boolean} value が threshold より小さいか
/
function isLessThan(value, threshold) {
// 1. まずそもそも「空文字」や「null」「undefined」をどう扱うか業務要件に合わせて弾く
if (value === null || value === undefined || value === ”) {
return false;
}
// 2. 明示的に数値へ変換を試みる(Number() や parseFloat() を使用)
const numericValue = Number(value);
// 3. Number.isNaN() を使って、変換結果が NaN でないかを厳格にチェックする
// ※ グローバルの isNaN() は暗黙の型変換を行うため、Number.isNaN() を使うのがプロの作法
if (Number.isNaN(numericValue)) {
console.warn(`[isLessThan]: 無効な値が比較されました ->`, value);
return false;
}
// 4. 安全が担保された数値同士で比較演算子を使う
return numericValue < threshold;
}
// --- 動作確認 ---
console.log(isLessThan(15, 18)); // true (正常な数値)
console.log(isLessThan('17', 18)); // true (数値に変換可能な文字列)
console.log(isLessThan('hoge', 18)); // false (NaNになるため安全に弾かれる、警告が出力される)
console.log(isLessThan(null, 18)); // false (早期リターンで安全)
チーフアーキテクトからのアドバイス
実務において、JavaScriptの暗黙の型変換は「言語の優しさ」ではなく、「油断すると足元をすくわれる罠」 だと考えたほうが身のためだ。
特に `<` や `>` を使うときは、
1. 比較する双方が本当に数値(Number)なのか?
2. 片方が文字列やオブジェクトになっていないか?
3. もしパースに失敗して `NaN` になった場合、ロジックが誤動作しないか?
この3点を、コードレビューの際には必ず自分の頭の中でシミュレーションしてほしい。動く動かないの表面的な話ではなく、「なぜその挙動になるのか」を仕様レベルで理解しているエンジニアこそが、チーム全体を救う信頼されるシニアへの道を歩んでいると、私は確信している。

コメント