こんにちは!フロントエンドからNode.jsの内部挙動まで、日々JavaScriptと向き合っているシニアアーキテクトです。
今回は、JavaScriptのデータ比較における「超重要かつ少しディープな罠」である `===`(厳密等価演算子)と `Object.is()` の決定的な違い についてお話しします。
「比較なんて `if (a === b)` を使っておけば間違いないんでしょ?」と思っていませんか?
実は、数値計算の世界には 「誰もが一度はハマる2つのモンスター」 が潜んでいます。それが `NaN`(非数) と `-0`(マイナスゼロ) です。
ここをクリアすれば、JavaScriptの数値表現と型システムの裏側までバッチリ見通せるようになりますよ。さあ、一緒にその深淵を覗いてみましょう!
—
1. そもそもJavaScriptの数値はどうやって動いているのか?
JavaScriptの数値(Number型)は、すべて内部的に 「IEEE 754」という国際規格に基づく倍精度浮動小数点数(64ビット)」 で表現されています。
これはメモリ上に「符号」「指数部」「仮数部」というパズルピースのようにデータを詰め込む仕組みなのですが、この仕様ゆえに、コンピュータ科学の歴史において「どうしても避けられない例外的な数値」が存在します。それが先ほどの `NaN` と `-0` です。
まずは、普段私たちが使っている `===` が、この特殊な数値たちに対してどう振る舞うのかを見てみましょう。
—
2. `===` 演算子の限界:NaN と -0 の反乱
罠その1:NaN は「自分自身すら自分だと認めない」
プログラミングを始めて間もない頃、こんなコードに出会って頭が真っ白になったことはありませんか?
// 計算が失敗したときに返される NaN (Not-a-Number)
const result = “文字” / 2;
console.log(result); // 出力: NaN
console.log(result === NaN); // 出力: え、falseなの!?
不思議ですよね。「`NaN` とは何か?」を比較しているのに、結果は `false` になります。
これはIEEE 754の規格で 「NaNは、自分自身を含むいかなる値とも等しくない」 と厳密に定義されているためです。`NaN` は「未確定」や「エラー」の概念なので、比較のしようがないというわけです。
従来のJavaScriptでは、これが原因で「値がNaNかどうか」を判定するためにわざわざ `Number.isNaN(val)` という専用関数を使わなければなりませんでした。
罠その2:0 と -0 は「違う」のか「同じ」なのか?
もう一つのエッジケースが「マイナスゼロ(`-0`)」です。
物理演算やアニメーションの座標計算、あるいは金融系のアルゴリズムなどを組んでいると、「どちらの方向からゼロに近づいたか」を区別するために `-0` が発生することがあります。
しかし、`===` 演算子でこれらを比較すると……?
const posZero = 0;
const negZero = -0;
console.log(posZero === negZero); // 出力: true
「おっ、同じ判定になるから安心だな」と思いましたか?
実はここに大きな罠があります。見た目は同じ `true` ですが、内部のメモリ表現や、特定の演算(例えば `1 / 0` は `Infinity` ですが、`1 / -0` は `-Infinity` になります)では完全に別物として扱われます。
「通常の比較では同じであってほしいけれど、厳密にデータ構造をチェックしたい時には区別したい」――そんな現場のエンジニアの切実な願いを解決するために登場したのが、ES2015(ES6)で追加された `Object.is()` です。
—
3. 救世主 `Object.is()` の登場と正しい使い方
`Object.is()` は、2つの値が「同じものかどうか」を判定するためのメソッドです。基本的には `===` と同じように使えますが、先ほどの `NaN` と `-0` の扱いにおいて決定的な違い を持っています。
まずは、コードでその違いを実感してみましょう。
// === 演算子 と Object.is() の比較表
// 1. 通常の数値
console.log(5 === 5); // true
console.log(Object.is(5, 5)); // true
// 2. NaN の比較
console.log(NaN === NaN); // false (自分を認めない)
console.log(Object.is(NaN, NaN)); // true (同じNaNだよと判定できる!)
// 3. 0 と -0 の比較
console.log(0 === -0); // true (区別してくれない)
console.log(Object.is(0, -0)); // false (ちゃんと別のものとして区別する!)
どうでしょうか? `Object.is()` は、私たちが直感的に「これって同じだっけ?」と思う疑問に対して、IEEE 754の仕様の隙間を完璧に埋める挙動をしてくれます。
—
4. 実務でどう使い分けるべきか?(プロの知見)
「じゃあ、明日から全部 `Object.is()` に書き換えればいいんだね!」と思ったそこのあなた、ちょっと待ってください。
私たちアーキテクトが現場でコードレビューをする際、すべてを `Object.is()` に置き換えることは推奨していません。ここには明確な使い分けの基準があります。
基本は `===` で十分
JavaScriptの日常的なコーディング(変数のチェック、文字列の比較、通常の数値比較など)の95%以上は、これまで通り `===` を使うべきです。`===` はV8エンジン等のJSエンジン側で極限まで最適化(インラインキャッシュなど)されており、非常に高速に動作します。
`Object.is()` を召喚すべきシチュエーション
以下のような「極限の精度が求められるエッジケース」に直面したときだけ、`Object.is()` を抜擢してください。
1. 数値計算ライブラリやゲームエンジンの開発
- 座標やベクトル演算で、ゼロの符号(`+0` と `-0`)を厳密に維持・追跡する必要があるとき。
2. 状態管理やメモ化(Memoization)の実装
- Reactの `useState` や内部のレンダリング最適化、あるいは自作のキャッシュ機構(前回の計算結果と同じか?)において、`NaN` が渡ってきたときに「前回の結果と同じだから再計算をスキップする」という判定を正確に行いたいとき。
// 実践的なメモ化関数の例(NaNを安全にハンドリングする)
let lastValue = NaN;
function checkCache(newValue) {
// === だと NaN のときにキャッシュが効かず再計算ループに陥る可能性がある
// Object.is なら NaN 同士を正しく同一視できる
if (Object.is(lastValue, newValue)) {
console.log(“キャッシュヒット!再計算をスキップします。”);
return true;
}
lastValue = newValue;
console.log(“キャッシュミス。新しく計算します。”);
return false;
}
checkCache(NaN); // キャッシュミス
checkCache(NaN); // キャッシュヒット!
—
財布の紐を締め直すように、データの比較も「何となく」ではなく、ランタイムの仕様を見据えて選ぶ。これがワンランク上のエンジニアへの第一歩です。
ここをクリアできれば、もうJavaScriptのデータ型や比較で迷うことはありません。自信を持って、次のモダンな実装へ進んでくださいね!