イントロダクション
JavaScript では異なる型の値を == で比較すると、片方の値がもう片方の型に変換されたり、どちらも別の型に変換されたりする。== 以外の演算子でもこのようなことが起きるが、これを type coercion といい、JavaScript の中でも特に覚えにくい機能である。
[] == ""
// ↓ [] が 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] メソッド を持たないとき
以下が順に実行される。
-
valueOfメソッドを持ち、実行結果が非 Object ならばそれを変換結果として終了。 -
toStringメソッドを持ち、実行結果が非 Object ならばそれを変換結果として終了。 - どちらも成功しなければ
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
-
Symbol object と Symbol value は異なることに注意。Symbol object の型はあくまで Object であり、Java でいうラッパークラスのようなものである。型が Symbol である値を Symbol value といい、
Symbol(name)で得られるのはこちらである。 ↩
