JavaScript

[JS] 42==trueがfalseになるわけ

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' == truetrueになると予想したのではないでしょうか。なぜなら、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 == truetrueになり、0 == falsetrueになります。

なので、タイトルの「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 == []もいくつかのステップがあります。まず、false0になり0 == []になります。[].valueOf()[]のため、[].valueOf()の戻り値が使われます。0 == ""の比較です。""がNumber型に変換され0になり0 == 0trueです。空文字が0になるのは直感的ではないですが、そうなっています。

このように予期しない結果になるため、比較するときは必ず===を使いましょう。

(参考)
Abstract Equality - You Dont Know JS