「これって等しい?」を確かめる2つの方法
JavaScriptには等価演算子が2種類あります。===(厳密等価)と ==(緩い等価)です。見た目はほぼ同じなのに、動きはまるで別物です。
=== は「型も値も同じか」をチェックする演算子です。一方の == は、先に両辺を共通の型に変換してから比較します。この型変換(type coercion)こそが、JavaScript の等価比較が長年ややこしいと言われてきた元凶です。
結論から言えば、基本は === を使えば OK。とはいえ、「何を避けているのか」を一度はちゃんと理解しておく価値があります。
厳密等価(===):まずはこれを使う
=== は、型と値のどちらも一致したときだけ true を返します。型変換は一切行われないので、思わぬ挙動に悩まされることもありません。
型が違えば、その時点で即 false になります。型が同じなら、JavaScript は値そのものを比較します。プリミティブなら値の比較、オブジェクトなら参照の比較です(これについては後ほど)。
!== は厳密不等価演算子で、同じルールを逆にした挙動になります。
等価演算子 == の裏にある暗黙の型変換
== は左右の型が違っていても使えます。ただし比較する前に、両辺の型を揃えようとして型変換(coercion)が裏で走ります。
厳密なルールは仕様書に書かれていて、読めばそこまでひどくはないのですが、デバッグ中にパッと思い出せるかというと話は別です。"0" == false が true になるのは、ベテランの開発者でも引っかかる罠です。[] == false も同様に true になります(配列が "" に変換され、さらに 0 に変換されるため)。
こうした背景から、多くのスタイルガイドや ESLint の eqeqeq ルールでは、デフォルトで === を使うことが推奨されています。1文字多くタイプする代わりに、頭で覚えておけるルールだけで済むわけです。
== が役に立つ唯一のパターン
等価演算子の中で、覚えておく価値があるイディオムが1つだけあります。x == null は、x が null または undefined のときに true を返し、それ以外はすべて false になります。
厳密に書くなら x === null || x === undefined になりますが、どうしても冗長に見えてしまいます。そこで == null だけは例外としてOKにしているコードベースも少なくありません。どちらの方針を採るにせよ、プロジェクト内で統一するのが大事です。
オブジェクトは参照で比較される
オブジェクト・配列・関数を比較するとき、=== も == も聞いていることは同じで、「両辺が同じメモリ上のオブジェクトを指しているか?」です。決して「中身が同じか?」を見ているわけではありません。
中身がまったく同じオブジェクトリテラルでも、別物として扱われます。これは誰もが一度はハマるポイントです。
値ベースで比較したいときは、自前でヘルパー関数を書くか、ライブラリ(lodash.isequal)を使うか、あるいはシンプルなプレーンオブジェクトなら JSON.stringify でシリアライズして比べる方法があります。
JSON.stringify が効くのは単純なデータだけです。関数、undefined、シンボルは無視されますし、形によってはエンジン間でキーの順序も保証されません。サッと確認する用途には使えますが、汎用的な解決策にはなりません。
NaN はどんな値とも等しくならない
NaN(not a number)は、数値演算で意味のある答えが出せないときに JavaScript が返す値です。たとえば 0/0、Number("abc")、Math.sqrt(-1) などですね。等価演算子は == も === も、どちらか一方が NaN なら false を返します。両辺とも NaN の場合でも同じです。
NaN を判定したいときは Number.isNaN(value) を使いましょう。昔ながらのグローバル関数 isNaN は引数を先に型変換してしまうので、isNaN("hello") が true を返します。これはまず期待した挙動ではないはずです。
Object.is:ほぼ ===、ただし2つだけ違う
Object.is(a, b) は基本的に === と同じように動きますが、次の2つのケースだけ挙動が異なります。
ほとんどのケースで使うべきは === です。Object.is の出番は、NaN を自分自身と等しいものとして扱いたい場合や、+0 と -0 を区別したい場合に限られます。どちらもまれなケースですが、数値計算やフレームワークの内部実装では意外と重要になることもあります(実際、Reactは状態の比較に Object.is を使っています)。
不等価演算子も同じ使い分け
!== は厳密、!= はゆるい比較で、こちらも同じ方針が当てはまります。
迷ったら !== を基本にしましょう。== null を許可しているプロジェクトなら、その裏返しである「null でも undefined でもない」を判定する != null も合わせて許可して構いません。
値を比較するときのチェックリスト
2つの値を比較したくなったら、次の順で考えてみてください。
- プリミティブ同士で、型も揃っているはず? →
===を使う。 - null や undefined の判定? → スタイルガイドが許すなら
x == nullでOK。ダメならx === null || x === undefined。 NaNの判定? →Number.isNaN(x)。- オブジェクトを参照(同一性)で比較したい? →
===がまさにそれ。 - オブジェクトを中身で比較したい? → 自前のヘルパーを書くか、ライブラリを使うか、シリアライズして比べる。標準の演算子ではどうにもなりません。
基本は ===、== は「== null のときだけ使う特別な道具」と割り切る。これだけで、JavaScript の FAQ によく並ぶ等価比較の落とし穴はほとんど避けられます。
次は演算子の話
厳密等価は、JavaScript に用意されている演算子のほんの一部にすぎません。次のドキュメントでは、算術・論理・代入をはじめ、日々のコーディングで頼りになる省略形の演算子まで、残りをまとめて見ていきます。
よくある質問
JavaScriptの == と === は何が違う?
=== は厳密等価で、型と値の両方が一致したときだけ true を返します。一方の == はゆるい等価で、比較前に型変換(暗黙の型強制)を行います。たとえば 1 === '1' は false ですが、1 == '1' は文字列が数値に変換されるため true になります。
JavaScriptでは常に === を使うべき?
基本的にはイエスです。=== はルールがシンプルで挙動を予測しやすいので、デフォルトで使うのが無難。例外としてよく使われるのが x == null で、これは null と undefined の両方に一発でマッチします。ESLintの eqeqeq ルールでも、このパターンだけはオプションで許可できるようになっています。
なぜ NaN === NaN は false になるの?
IEEE 754仕様で「NaN はどんな値とも等しくない(自分自身とも)」と決められているためです。そのため NaN が絡むとどの等価演算子も false を返します。値が NaN かを判定したいときは Number.isNaN(x) を使うか、Object.is(NaN, NaN) であれば true が返ります。
JavaScriptでオブジェクト同士を比較するには?
== も === もオブジェクトは「参照」で比較します。中身ではありません。なので {a: 1} === {a: 1} は別インスタンスなので false です。値として比較したい場合は自前で比較関数を書くか、Lodashの isEqual のようなライブラリを使うか、単純なプレーンオブジェクトなら JSON.stringify で文字列化して比較するのが手軽です。