JSを少しでも書いてる人は値を比較するときに必ず===
を使えと教わることだと思います。==
がいかにエンジニアの直感に背き、値を比較するかを書いてみようと思います。
まずは、以下のコードが何をプリントするか予想してみてください。
console.log(42 == true); // ?
console.log('42' == true); // ?
console.log(1 == true); // ?
console.log(0 == false); // ?
.
.
.
予想できましたか?結果はこうなります。
console.log(42 == true); // false
console.log('42' == true); // false
console.log(1 == true); // true
console.log(0 == false); // true
42 == true
や'42' == true
はtrue
になると予想したのではないでしょうか。なぜなら、42
も'42'
も単体ではtruthyだからです。
その証拠にif
で単体で比較するとtrue
になります。
if (42) {
console.log('42 is true!');
} else {
console.log('42 is false!');
}
if ('42') {
console.log('true');
} else {
console.log('false');
}
これらはを実行するとどちらも42 is true!
とtrue
が出力されます。なので、型強制(coercion)が行われtrue == true
が実行されたと考えるのが自然です。
ところがそうなりません。これはスペックを読むと理解することができます。(Abstract Equality Comparisonの6と7より)
x == y
If Type(x) is Boolean, return the result of the comparison ToNumber(x) == y.
If Type(y) is Boolean, return the result of the comparison x == ToNumber(y).
(日本語)
もし、xがBooleanなら、ToNumber(x) == yを返す
もし、yがBooleanなら、y == ToNumber(y)を返す
つまり、実際に起きていたのは「Boolean型じゃない値をBoolean型に変換し比較」ではなく、「Boolean型の値をNumber型に変換し比較」だったのです。
Boolean型をNumber型に変換するとtrue
が1になり、false
が0になります。そうすると、42 == 1
と'42' == 1
の比較になり当然結果はfalse
になります。
同じ理由で1 == true
はtrue
になり、0 == false
もtrue
になります。
なので、タイトルの「JSで42==trueがfalseになるわけ」は「==で比較される値のうち片方がBooleanの場合、Booleanの値をNumberに型強制して比較するようにSpecに書かれているから」が答えになります。
.
.
.
せっかくなので、もう少し==
を見てみましょう。片方がオブジェクトでもう片方がString
, Number
, Symbol
の場合、オブジェクトをプリミティブ型に変換して比較することが指定されています。(7.2.14 Abstract Equality Comparisonの8と9より)
x == y
If Type(x) is either String, Number, or Symbol and Type(y) is Object, return the result of the comparison x == ToPrimitive(y).
If Type(x) is Object and Type(y) is either String, Number, or Symbol, return the result of the comparison ToPrimitive(x) == y.
(日本語訳)
もし、xがString, Number, Symbolならx == ToPrimitive(y)を返す
もし、yがString, Number, SymbolならToPrimitive(x) == yを返す
ここで使われるToPrimitive
はJSエンジン内部で使われる処理のことで、オブジェクトのプロトタイプチェーンには定義されていません。ここではvalueOf()
, toString()
を順番に呼び、返ってきた値がプリミティブならその値を返す、プリミティブにならなかったらTypeErroe
を返すという処理になっています。(7.1.1 ToPrimitive)
これを使ってみるとこのような面白い結果になります。
console.log(1 == [1]); // true
console.log(123 == [123]); // true
console.log(true == [1]); // true
console.log(false == []); // true
1 == [1]
は[1].valueOf()
が[1]
を返すので、[1].toString()
を呼びます。そこでプリミティブの"1"
が返ってくるため、1 == "1"
の比較結果になります。型強制(coercion)が起こりtrue
になります。
123 == [123]
も前のサンプルと同様123 == "123"
の比較になります。
true == [1]
はトリッキーです。true
があるため、まずここを処理しないといけません。true
はNumber型に変換され、1 == [1]
になります。これは最初のサンプルと同じなため、同じステップを踏みtrue
になります。
false == []
もいくつかのステップがあります。まず、false
が0
になり0 == []
になります。[].valueOf()
は[]
のため、[].valueOf()
の戻り値が使われます。0 == ""
の比較です。""
がNumber型に変換され0
になり0 == 0
はtrue
です。空文字が0になるのは直感的ではないですが、そうなっています。
このように予期しない結果になるため、比較するときは必ず===
を使いましょう。