イントロダクション
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)
で得られるのはこちらである。 ↩