JavaScript の"=="の挙動をハッキリさせておく


イントロダクション

JavaScript では異なる型の値を == で比較すると、片方の値がもう片方の型に変換されたり、どちらも別の型に変換されたりする。== 以外の演算子でもこのようなことが起きるが、これを type coercion といい、JavaScript の中でも特に覚えにくい機能である。

type coercionの例
[] == ""

//  ↓  [] が string に変換される

"" == ""

//  ↓  文字列比較

true

ここでは ECMAScript 2019 Language Specification に基づいて、== の型変換がどのように行われているのかを説明する。ECMAScript 2020 では新しく BigInt という型が追加されるが、ここでは扱わない。

JavaScript における型は以下がすべてである。typeof で得られるものとは違うので注意されたい。

  • Number: 100, -1, +0, -0, Infinity, -Infinity, NaN など
  • String: "abc", "" など
  • Boolean: false, true
  • Symbol: Symbol("name"), Symbol.iterator など
  • Undefined: undefined
  • Null: null
  • Object: 上記以外

型変換が起きる組み合わせ

まずは、どの型の組み合わせで型変換が起きるのかを下の表で確認しよう。具体的な変換アルゴリズムは後で説明する。表にある primitive とは Object 以外の値という意味である。

NOTE: 型変換は複数回起こりうるので注意。例えば object と string を比較するとき、まず object が primitive に変換される。変換された結果 number であった場合、number と string なので今度は string が number に変換され、最終的に number どうしの厳密比較 === が行われる。

以下でそれぞれの型変換がどのように行われるかを説明する。

Boolean ➡️ Number

  • false ➡️ 0
  • true ➡️ 1

String ➡️ Number

Number() 関数に渡したときと全く同じように変換される。

  • "32" ➡️ 32
  • "1.602e-19" ➡️ 1.602e-19 (指数表記)
  • "Infinity" ➡️ Infinity
  • "0b1011" ➡️ 11 (2進数)
  • "0o707" ➡️ 455 (8進数)
  • "0xa0" ➡️ 160 (16進法)
  • "abc" ➡️ NaN

Object ➡️ primitive

これは少し複雑であるため、順を追って説明する。まずはその object が [Symbol.toPrimitive] メソッドを持っているかがチェックされる。このメソッドを最初から持っている object は Date object と Symbol object1 のみである(逆に言えば、これ以外の object はデフォルトでこのメソッドを持っていない)。

[Symbol.toPrimitive] メソッド を持つとき (≒ Date object か Symbol object のとき)

そのメソッドに引数 "default" を渡して実行し、その戻り値が変換結果となる。ただし、もしこの戻り値が Object であれば TypeError となる。

obj[Symbol.toPrimitive]("default")

例えば Date object の場合以下のようになる。

const obj = new Date('2019-10-14T10:20:30Z')
obj[Symbol.toPrimitive]("default")  // "Mon Oct 14 2019 19:20:30 GMT+0900 (Japan Standard Time)"

実際、下の式は true になる(タイムゾーンの設定が日本であることが前提)。

obj == "Mon Oct 14 2019 19:20:30 GMT+0900 (Japan Standard Time)"  // true

Symbol object の場合、このメソッドは内部の Symbol value を返す。

const sym = Symbol("name")  // Symbol value
const symObj = Object(sym)  // Symbol object

symObj[Symbol.toPrimitive]("default") === sym  // true
symObj == sym  // true

[Symbol.toPrimitive] メソッド を持たないとき

以下が順に実行される。

  1. valueOf メソッドを持ち、実行結果が非 Object ならばそれを変換結果として終了。
  2. toString メソッドを持ち、実行結果が非 Object ならばそれを変換結果として終了。
  3. どちらも成功しなければ TypeError となる。

例えば普通のオブジェクトや配列は valueOf メソッドは自分自身を返し toString メソッドは String を返すので、toString() メソッドの実行結果が変換結果となる。

({ a: 1 }).toString()  // "[object Object]"
({ a: 1 }) == "[object Object]"  // true

[1, 2, 3].toString()  // "1,2,3"
[1, 2, 3] == "1,2,3" // true

例えば以下のように人工的に valueOf メソッドをセットすることで、変換結果を変えることもできる。

({valueOf: () => 2019}) == 2019  // true

厳密等号 ===

== で比較するとき、最初から同じ型であるかもしくは型変換の結果同じ型になると === による厳密比較が行われる。同じ型の値を === で比較したときのルールをそれぞれの型について説明する。

Number と Number

どちらかが NaN ならば false (どちらも NaN であっても false) となり、それ以外の場合は普通の数値比較の結果を返す。

Note: 実は 0-0 は内部的には異なる値だが、=== で比較しても等しいとみなされる。

String と String

単純に文字列として等しければ true、そうでなければ false となる。

Boolean と Boolean

単純に比較した結果となる。

Undefined と Undefined

常に true となる。

Null と Null

常に true となる。

Object と Object

参照(メモリアドレス)の比較となり、たとえ値が同じでもメモリの別の場所に存在すれば false となる。

const o1 = { a: 1 }
const o2 = { a: 1 }

o1 === o1  // true
o1 === o2  // false

Symbol と Symbol

Object と同様に参照の比較となる。たとえ同じ description (Symbol() の引数) を持っていても、別々に作られたならば異なるとみなされる。

const s1 = Symbol("my symbol")
const s2 = Symbol("my symbol")

s1 === s2  // false

仕様の該当箇所

  • == による比較: 7.2.14
  • === による比較: 7.2.15
  • Date object の [Symbol.toPrimitive] メソッド: 20.3.4.45
  • Symbol object の [Symbol.toPrimitive] メソッド: 19.4.3.5

  1. Symbol object と Symbol value は異なることに注意。Symbol object の型はあくまで Object であり、Java でいうラッパークラスのようなものである。型が Symbol である値を Symbol value といい、Symbol(name) で得られるのはこちらである。