厳密等価演算子(`===`)の限界と、`Object.is` が救うエッジケースの真実
JavaScriptの型システムと値の比較において、我々フロントエンドエンジニアが最も頻繁に手にする武器は間違いなく厳密等価演算子(`===`)だ。しかし、V8などのモダンなJavaScriptエンジンが内部でどのように値を表現し、IEEE 754という浮動小数点数の規格の闇とどう向き合っているかを知る上級エンジニアにとって、`===` は万能薬ではない。
特に、状態管理ライブラリの差分検出(Reconciliation)や、極限まで最適化されたメモ化(Memoization)のアルゴリズムを自作する際、`===` の持つ2つの重大な仕様上の欠陥が、時として原因不明のバグや致命的なレンダリング負荷を引き起こす。
その欠陥とは以下の2点だ。
1. `NaN === NaN` が `false` になるという、数学的直感に反する仕様。
2. `+0` と `-0` を「同じもの」として扱ってしまう仕様。
今回は、この隠れた仕様の罠を暴き、ECMAScript 6 (ES2015) で導入された `Object.is` が、いかにして堅牢なアプリケーションの土台を支えるかを、アーキテクチャの視点から深掘りしていこう。
—
1. IEEE 754の呪縛:`===` が崩壊する2つの瞬間
ブラウザのJavaScriptエンジンにおいて、数値(Number)はすべてIEEE 754標準に基づく64ビットの倍精度浮動小数点数としてメモリ上に格納されている。この設計思想が原因で、日常的なプログラミングにおいて直感とズレる挙動が発生する。
罠その1:孤児となった `NaN`(Not-a-Number)
計算エラーや不正なパース結果として現れる `NaN` は、IEEE 754の仕様上、「自分自身を含め、いかなる値とも等しくない」という残酷な宿命を背負っている。
// 誰もが一度はハマるJavaScriptの闇
console.log(NaN === NaN); // -> false!
これがなぜ問題になるか。例えば、フォームの入力値バリデーションや、複雑な数値演算パイプラインにおいて、前回の計算結果と今回の計算結果がどちらも `NaN` であった場合、`===` を使った比較は「値が変わった(差分がある)」と誤認してしまう。結果として、不要な再レンダリングや、無限ループに近い副作用のトリガーを引くことになる。
罠その2:符号付きのゼロ(`+0` と `-0`)
もう一つの無視できない異物は、正のゼロ(`+0`)と負のゼロ(`-0`)の存在だ。これらはビット表現において符号ビットが異なるため、厳密には別のメモリ状態を持っている。
// エンジンレベルでは別物なのに、=== では区別できない
console.log(+0 === -0); // -> true
「ゼロにプラスもマイナスもあるか」と思うかもしれないが、グラフィックス演算、物理シミュレーション、あるいはゼロ除算の方向性(`1 / +0` は `Infinity`、`1 / -0` は `-Infinity`)を厳密に扱うドメインにおいては、この差異は極めて重要だ。特に、状態の不変性(Immutability)を担保するイミュータブル・データ・ストアにおいて、符号違いのゼロの混入が予期せぬ計算誤差を生む温床となる。
—
2. `Object.is` の内部アルゴリズム:SameValue比較の正体
ここで登場するのが `Object.is` だ。ECMAScript仕様書では、この比較アルゴリズムは 「SameValue(同一値)比較」 と定義されている。`===` が採用する「Strict Equality(厳密等価)比較」とは何が違うのか。
仕様書のアルゴリズムを我々の言葉で翻訳すると、`Object.is(x, y)` は以下のステップで評価される。
1. `x` と `y` の型が異なるなら `false`。
2. `x` と `y` がともに `Number` 型の場合:
- どちらか一方が `NaN` で、もう一方も `NaN` なら `true`(ここで `===` と袂を分かつ)。
- 一方が `+0` でもう一方が `-0` なら `false`(符号を厳密に区別する)。
- それ以外は通常の数値としての比較。
3. それ以外の場合は、`===` と完全に同じ挙動(参照の比較、またはプリミティブの値の比較)。
この挙動の違いを、実務で遭遇しうるコードで確認してみよう。
// — NaN の比較 —
const previousState = { score: NaN };
const currentState = { score: NaN };
// === だと「値が変わった」と誤認する
console.log(previousState.score === currentState.score); // false
// Object.is なら正しく「同じ値(NaN)」と判定できる
console.log(Object.is(previousState.score, currentState.score)); // true
// — +0 と -0 の比較 —
const valA = +0;
const valB = -0;
console.log(valA === valB); // true (区別できない)
console.log(Object.is(valA, valB)); // false(厳密に区別する)
—
3. 実践:高度なメモ化と状態管理におけるアーキテクチャ上の選択
「じゃあ、すべての `===` を `Object.is` に置き換えれば完璧なのだな?」
待ってほしい。チーフアーキテクトとして、パフォーマンスと実用性のトレードオフについて警鐘を鳴らしておかなければならない。
パフォーマンスコストの現実
`Object.is` は関数呼び出しのオーバーヘッドを伴う。V8エンジンのインラインキャッシュ(Inline Caching)やJITコンパイラの最適化により、その差はマイクロ秒単位の世界ではあるが、数万回、数百万回とループするホットパス(Hot Path)や、高速な仮想DOMの差分検出アルゴリズムの中では、このわずかな差がレンダリングフレームレート(60fpsの維持)に悪影響を及ぼす可能性がある。
そのため、通常のオブジェクトのプロパティ比較や、`undefined` / `null` のチェックなどでは、高速かつシンプルに評価できる `===` を使い続けるべきだ。
堅牢性を極めるカスタム・レコグナイザーの実装
では、どのような場面で `Object.is` を採用すべきか?
それは、「不特定多数のデータが流れ込む汎用的なメモ化関数(`useMemo` やカスタムフックの依存配列比較など)」や、「NaNが頻出する科学技術計算・金融データのリアクティブ処理」である。
以下に、実務の現場で使える、`Object.is` をベースにした堅牢な浅い比較(Shallow Equal)ユーティリティのサンプルを示す。
/
- 堅牢性を極めたオブジェクトの浅い比較関数
- NaNや符号付きゼロの差異を正確に検出し、無駄な再計算を防ぐ
/
export function robustShallowEqual(objA, objB) {
// 参照が完全に同じ、または両方とも同じプリミティブ値の場合
if (Object.is(objA, objB)) {
return true;
}
// どちらかがオブジェクトではない、または null の場合
if (
typeof objA !== ‘object’ ||
objA === null ||
typeof objB !== ‘object’ ||
objB === null
) {
return false;
}
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
// プロパティの数が異なる場合は別物
if (keysA.length !== keysB.length) {
return false;
}
// 各プロパティの値を Object.is で厳密に比較する
for (let i = 0; i < keysA.length; i++) {
const key = keysA[i];
// プロパティが存在するか、かつ値が Object.is で等しいか
if (
!Object.prototype.hasOwnProperty.call(objB, key) ||
!Object.is(objA[key], objB[key])
) {
return false;
}
}
return true;
}
// --- 動作検証 ---
const state1 = { value: NaN, vector: +0 };
const state2 = { value: NaN, vector: -0 };
console.log(robustShallowEqual(state1, state2));
// -> false (vectorの符号違い、およびvalueのNaNを正しく安全にハンドリングした上で判定)
Reactのソースコードを覗いたことがある読者なら気づくだろう。Reactの内部で使われている `Object.is` のポリフィル(あるいはネイティブ実装)は、まさにこのような「コンポーネントのPropsやStateが本当に変わったのか」を正確に判定するために、なくてはならない存在として組み込まれているのだ。
—
4. チーフアーキテクトからの提言
JavaScriptにおける等価性比較は、単なる「値が同じか確かめるボイラープレート」ではない。それは、メモリ上のビット列と仕様書のルールを理解しているエンジニアと、ただ動くだけのコードを書くエンジニアを分けるリトマス試験紙だ。
- 日常的な比較には、速度と可読性を重視して `===` を使う。
- データ構造の差分検出、メモ化、NaNや符号が結果に影響を与えるクリティカルなドメインでは、必ず `Object.is` を選択する。
この使い分けをチーム全体の共通認識とし、堅牢なアーキテクチャの礎としてほしい。コードの裏側にあるエンジンと仕様の挙動を愛せば、あなたの書くフロントエンドは、もっと美しく、もっと強靭になる。

コメント