JavaScriptの等値比較を全部理解する

皆さんこんにちは。今回の記事ではJavaScriptの**等値比較**について見ていこうと思います。

「どうせ`==`と`===`の違いとかだろ？　今さらそんな記事書くなよバーカｗ」と思った人はぜひ期待せずに読み進めてみてください。
「じゃあ`Object.is`でしょ？　知ってる知ってる、使ったことないけど」と思った人はまあ読まなくても大丈夫です。

さて、**等値比較**というのは、2つの値が**等しいかどうか判定する**ことです。JavaScriptにおいて等値比較はどのように行うのか、そしてどのような場面で等値比較が発生するのかをこの記事では余すことなく紹介します。

以降、この記事で仕様書という場合は[ECMAScript® 2018 Language Specification](https://www.ecma-international.org/ecma-262/9.0/index.html)を指すものとします。

# `==`と`===`
とはいえ、まずは`==`と`===`の話をしないことには始まりません。`==`はJavaScript初心者がとりあえず習う等値比較の演算子ですが、その実あまり使われることはありません。実際にはほとんど`===`が使われます。

その理由は、`==`には**暗黙の型変換**機能が付いているからです。数値と文字列の比較では両方が文字列に変換されたり、プリミティブとオブジェクトを比較するとオブジェクトがプリミティブに変換されたりします。（プリミティブとは、数値や文字列などの、オブジェクト以外の値のことです。）

```js:==と===の違い
console.log(3 == "3"); // true（"3"が3に変換されるので）
console.log(1000 == "1e3"); // true（"1e3"が1000に変換されるので）

console.log(3 === "3"); // false（数値と文字列で型が違うので）
console.log(1000 === "1e3"); // false（数値と文字列で型が違うので）
```

`==`の挙動については以下の記事でも解説しましたので、気になる方は目を通してみてください。

- [JavaScriptのプリミティブへの変換を完全に理解する](https://qiita.com/uhyo/items/44c2f79873de13186743)

`==`は型変換機能がやっかいなのであまり使いませんが、`== null`は結構使うことがあります。`==`は`null`と`undefined`を同一視するので、`== null`は（`null`または`undefined`である）という意味の比較になるわけですが、この2種類の値は「プロパティを参照するとエラーが発生する値である」という点で共通しているため、これらの値をまとめて弾きたいときに重宝します。実際、[eslintで===の使用を強制するeqeqeqルール](https://eslint.org/docs/rules/eqeqeq#require--and--eqeqeq)でも`null`の場合は許可する`"null"`オプションが用意されています。

## 等値比較の基礎
特に`===`はJavaScriptにおける基本的な等値比較ですから、これの挙動をもう少し解説します。といっても、基本的には値が同じならtrueで違ったらfalseというわけですが、

まず、**オブジェクトは参照として比較されます**。つまり、`===`でオブジェクト同士を比較する場合、参照として同じオブジェクトのときのみtrueとなります。以下の例で見るように、形だけ同じ場合は`false`となります。

```js:オブジェクトの比較の例
const obj1 = { foo: 'bar' };
const obj2 = { foo: 'bar' };

console.log(obj1 === obj2); // falseになる（同じ形のオブジェクトでも別々に作ったら別のオブジェクトなので）

const obj3 = { hoge: 123 };
const obj4 = obj3;

console.log(obj3 === obj4); // true（obj3とobj4は参照として同じなので）
```

JavaScriptでは配列もオブジェクトの一種なので、配列についても同じことがいえます。

```js:配列の比較の例
const arr1 = [0, 1, 2];
const arr2 = [0, 1, 2];

console.log(arr1 === arr2); // false
```

プリミティブの比較も普通です。値として同じものを`===`で比較したときのみ`true`になります。

```js:プリミティブの比較

console.log(3 === 3); // true（同じ値を比較すると当然true）（なんかあほみたいな例ですが）
console.log(30 === 300); // false

console.log('foo' === 'FOO'); // false

console.log(3000 === '3000'); // false（型が違うものは全部false）
```

ただし、`NaN`については`NaN === NaN`の結果が`false`になるため注意してください。

```js:NaNの比較結果
const num1 = Number.NaN;

console.log(num1 === Number.NaN); // false
```

`NaN`のこの挙動についてはJavaScriptがおかしいわけではなく、浮動小数点数（IEEE 754）の仕様から来るものです。

# `Object.is`
さて、等値比較の基本が分かったところで、上でちらっと出てきた`Object.is`の紹介に移ります。`Object.is`はES2015で追加された比較的新しい機能です。

この`Object.is`は渡された2つの値が等しいかどうか調べる関数です。この関数による等値判定は基本的には`===`と同じです。まあ、やり方によって等値性がころころ変わってもらっては使い物にならないので当然ですが。

とはいえ、`Object.is`は`===`と少しだけ異なるところがあり、ある意味で**`===`よりもさらに厳密な等値判定**が可能になっています。`Object.is`が`===`と異なるのは次の2点だけです。

- `NaN`同士を比較すると`true`になります。
- `+0`と`-0`を比較すると`false`になります。

```js:Object.isの例
// NaNに対する動作の違い
console.log(Object.is(NaN, NaN)); // true
console.log(NaN === NaN); // false

// +0と-0の区別
console.log(Object.is(+0, -0)); // false
console.log(+0 === -0); // true（===は+0と-0を同じとみなす）
```

それ以外の点では`Object.is`は`===`と同じです。暗黙の型変換は行なわれず、オブジェクトは参照として比較されるなどの点も同じです。

ここで、一部の方は「**おいおいちょっと待て、+0と-0って何やねん**」と思っているかもしれません。実はJavaScriptにはゼロが+0と-0の2種類あるのです（実はこれもJavaScriptには、というよりはIEEE 754浮動小数点数に存在する概念です）。

我々が普通に`0`と書いたときに得られるのは+0です。また、`-5 + 5`などの計算で得られるのも+0です。一方で、「負の数 × +0」や「正の数 × -0」という掛け算の結果としては-0が得られます。もちろん割り算も同様です。また、細かいところでは、`-0 + 0`や`0 + (-0)`は+0ですが`(-0) + (-0)`は-0です。

このように、数としては同じ0でも計算の方法によって+0になったり-0になったりします。これらはどちらもゼロなのだから等しいとして扱いたいことが多いと思われますから、`===`は+0と-0を区別しないのです。これのおかげで、我々は普段の数値計算プログラムで+0と-0の違いを心配しなくても（滅多なことが無い限りは）大丈夫なのです。

その一方で、値として異なるのだから区別しなければいけないという場面がもしかしたらあるかもしれません。そのような場合は`Object.is`を使うことで+0と-0を区別した等値判定ができるというわけです。また、`Object.is`は`NaN`同士の比較をちゃんと`true`にしてくれますから、数値計算がどうとかではなくとりあえず値として同じか調べたいというときには一番素直かもしれません。（その場合でも+0と-0を区別すべきかどうかは別に考える必要がありますが。）

余談ですが、`Object.is`に頼らずに+0と-0を区別する方法は一応あります。正の数をゼロで割ってみて、正の無限大になったら+0で負の無限大になったら-0です。

```js:+0と-0の判別
console.log(1 / 0);  // Infinity
console.log(1 / -0); // -Infinity
```

これを使って`Object.is`を再実装してみるとだいたいこんな感じになります。

```js:Object.isの再実装
function Object_is(left, right) {
  // 左辺がNaNのときの処理
  if (Number.isNaN(left)) {
    // 右辺もNaNのときのみtrue
    return Number.isNaN(right);
  }
  // 両辺が0または-0のときの処理
  if (left === 0 && right === 0) {
    // 上述の方法で+0と-0を判別
    const leftSign = 1 / left;
    const rightSign = 1 / right;
    return leftSign === rightSign;
  }
  // 他の場合は===と同じ動作
  return left === right;
}
```

以上で、JavaScriptで等値比較を行う3種類の方法、すなわち`==`と`===`と`Object.is`を紹介しました。この記事の話題はもう1つあります。

# 等値比較が発生する場面
上で紹介したような手法を使う以外にも、半ば暗黙に等値比較が行なわれる場面はあります。ここでは、そのような場合にどのような等値比較が行なわれるのかを紹介します。

## switch文
`switch`文を使ったことがある方は多いと思います。`switch`文はこういうやつです。

```js:switch文の例
switch (userName) {
  case 'admin':
    console.log('ようこそ、管理者様');
    break;
  case 'John Smith':
    console.log('ようこそ、ジョンさん');
    break;
  default:
    console.log('お前誰？');
}
```

このように、与えられた値が特定の値であるときの処理を好きなだけ書くことができます。実はここで、暗黙のうちに**等値判定**が発生していますね。例えば`userName`が`'admin'`と等しいかどうかとか、`userName`が`Johm Smith'`と等しいかどうかが判定されます。では、ここでの等値判定はどの等値判定でしょうか。

答えは`===`です。上の例ではまず`userName === 'admin'`という比較が行なわれ、その次に`userName === 'John Smith'`という比較が行なわれることになります。以下の例で確かめてみましょう。

```js
switch(0) {
  case "0":
    console.log("これは表示されない（数値と文字列を===で比較するとfalseなので）");
    break;
  case -0: 
    console.log("これは表示される（+0と-0を===で比較するとtrueなので）");
}
```

ちなみに、switchの比較が`===`で行なわれることはどうやって確かめるのでしょうか。答えはもちろん[仕様書](https://www.ecma-international.org/ecma-262/9.0/index.html)です。[switchの評価手順（13.12.11）](https://www.ecma-international.org/ecma-262/9.0/index.html#sec-switch-statement-runtime-semantics-evaluation)を見ると、実際の処理は[13.12.9 Runtime Semantics: CaseBlockEvaluation](https://www.ecma-international.org/ecma-262/9.0/index.html#sec-runtime-semantics-caseblockevaluation)で行なわれていることが分かります。その中でも、`case`を実行するかどうか判定する[CaseClauseIsSelected](CaseClauseIsSelected)を引用すると次の画像のようになっています。

[![Screenshot from Gyazo](https://gyazo.com/fa08bc99a26d3196346e4531bc261056/raw)](https://gyazo.com/fa08bc99a26d3196346e4531bc261056)

特に4の部分を見ると、`===`で比較するとちゃんと書いてありますね。

> 4​. Return the result of performing Strict Equality Comparison input === clauseSelector.

### 余談
これは本題とは関係ありませんが、上の引用部分の2を見ると、`case`のあとに書かれた式を評価していることが分かります。

> 2​. Let exprRef be the result of evaluating the Expression of C.

そして、switch文の一致判定は上から順番に評価されます。また、switch文は一度`case`に合致したあとは（`break`でswitch文から抜けない限り）以降の`case`も全部実行するという動作をしますから、一度合致したあとは次以降の`case`文の式を評価する必要はありません。

以上のことから、`case`の式に副作用を仕込むと少しおもしろいです。

```js:caseの式の副作用の例
// aを参照するたびにconsole.logしてaの値が1増えるオブジェクト
var obj = {
  _a: 0,
  get a() {
    console.log('a is', this._a);
    return this._a++;
  },
};
switch(5) {
  case obj.a:
  case obj.a:
  case obj.a:
  case obj.a:
  case obj.a:
  case obj.a:
  case obj.a:
  case obj.a:
  case obj.a:
  case obj.a:
}
console.log('the final value of a is', obj._a);
/* コンソールの出力：
a is 0
a is 1
a is 2
a is 3
a is 4
a is 5
the final value of a is 6
*/
```

この例では`obj.a`が6回評価されていることが分かります。

## `Array#indexOf`
さて、本題に戻りましょう。前の節では`switch`文による比較が`===`で行なわれることを見ました。次は配列の`indexOf`メソッドです。これは、指定した値の配列内での位置を返すメソッドです。存在しなかったら`-1`が返ります。また、複数存在する場合は一番最初の番号が返されます。

```js:indexOfの例
var arr = [1, 1, 2, 3, 5, 8, 13];

console.log(arr.indexOf(5)); // 4
```

このメソッドでも等値比較が行われていることは明らかですね。この場合、配列の各要素が与えられた`5`と比較されて一致したらその要素番号が返されます。では、これはどのような等値比較が行なわれているのでしょうか。

答えはこれまた`===`です。もう一々仕様書を引用したりしませんが、[仕様書の22.1.3.12節](https://www.ecma-international.org/ecma-262/9.0/index.html#sec-array.prototype.indexof)に書いてあります。

また、左からではなく右から検索する`Array#lastIndexOf`についても同じく`===`です。まあ、ここが違ったらとても混乱しますので困りますが。

## `Array#includes`
また、`indexOf`とちょっと似ているメソッドとして`includes`があります。これは、指定した要素が配列に含まれていたら`true`、含まれていなかったら`false`を返すメソッドです。

```js:includesの例
var arr = [1, 1, 2, 3, 5, 8, 13];
console.log(arr.includes(5)); // true（5は含まれているので）
console.log(arr.includes(10)); // false（10は含まれていないので）
```

これはES2016で追加されたちょっと新しいメソッドです。これも`indexOf`と同様に等値比較を行うメソッドです。なぜなら、値が値に含まれているか判定するには、配列の各値が与えられた値と一致するかどうかを判定しなければならないからです。

どうでしょうか、「は？　こんなん`arr.indexOf(5) >= 0`とかでええやん」と思いましたか？　しかし、**実は違うんです。**なんと、**`Array#includes`が行う比較は`===`ではない**のです。仕様書の[22.1.3.11節](https://www.ecma-international.org/ecma-262/9.0/index.html#sec-array.prototype.includes)から引用します。

> NOTE 3  The `includes` method intentionally differs from the similar `indexOf` method in two ways. First, it uses the SameValueZero algorithm, instead of Strict Equality Comparison, allowing it to detect **NaN** array elements. Second, it does not skip missing array elements, instead treating them as undefined.

> （意訳）`includes`メソッドは`indexOf`とは意図的に異なる仕様にしている部分が2つあります。1つは`===`ではなくSameValueZeroアルゴリズムを使って等値比較することによって`NaN`を発見できるようにしている点です。もうひとつは存在しない要素を飛ばすのではなくundefinedとして扱う点です。

特に前半部分に目を向けると、[SameValueZero](https://www.ecma-international.org/ecma-262/9.0/index.html#sec-samevaluezero)アルゴリズムを使って等値比較を行うと書いてあります。このSameValueZeroというのが、`==`とも`===`とも`Object.is`とも異なる**第4の等値比較**です。

SameValueZeroは`===`とほとんど同じですが、NOTEに書いてある通り**NaN同士の比較がtrueになる**点が異なります。ただし、`===`と同様に**+0と-0は等しいものとして扱う**ことになっており、ここが`Object.is`と異なる点です。表にまとめるとこのようになります。

|   | `===` | SameValueZero | `Object.is` |
|---|-------|---------------|-------------|
| `NaN`と`NaN` | 等しくない | 等しい | 等しい |
| `+0`と`-0` | 等しい | 等しい | 等しくない |

`Array#includes`はこのSameValueZeroを使っているため、ちゃんと`NaN`を発見することができます。というか、`indexOf`は逆に`NaN`を発見できなかったことがちょっと驚きですね。

```js:includesとindexOfが異なる例
var arr = [1, 2, NaN, 3];

console.log(arr.indexOf(NaN) >= 0); // false（indexOfはNaNを発見できないので）
console.log(arr.includes(NaN)); // true（includesはNaNを発見できる）
```

SameValueZeroは`NaN`と`NaN`が等しい上に`+0`と`-0`を等しいものとして扱うという、なんだか一番直感的な気がしないでもない等値比較です。残念ながらこれを直に行ってくれる関数は用意されていません。必要ならこんな感じで作るといいかもしれません。

```js
function sameValueZero(left, right) {
  return [left].includes(right);
}
```

### 余談
これまた余談ですが、先ほど引用したNOTEの後半部分にもちょっと気になることが書いてありますね。詳しいことは省略しますが、これは穴あきの配列に対する`indexOf`と`includes`の挙動の違いを述べています。ここでは違いが見える例だけ載せておきます。

```js
var arr = [0, , 2, 3, , 5];

console.log(arr.indexOf(undefined) >= 0); // false
console.log(arr.includes(undefined)); // true
```

## `Map`とか
では次の例に移りましょう。ES2015以降のJavaScriptには`Map`が入っています。これはいわゆる辞書で、何らかの値（キー）に対して別の値を紐付けることができるものです。

```js:Mapの例
var map = new Map();

// 2をキーとして対応する値を登録
map.set(2, "two");

// getメソッドでキーに対応する値を取り出せる
console.log(map.get(2)); // "two"

var obj = { foo: 'bar' };

// オブジェクトもキーに使える
map.set(obj, "オブジェクト");
console.log(map.get(obj)); // "オブジェクト"

// 存在しないキーでgetを行うとundefinedが返る
var obj2 = { foo: 'bar' };
console.log(map.get(obj2)); // undefined
```

特徴は、キーとしてどんな値でも使うことができ、オブジェクトでさえキーにすることができるという点です。

また、`Map`に類似するものとして`Set`や`WeakMap`、そして`WeakSet`が存在します。すごく余談ですが、イテレータがまだ実装されていない時代に`Map`や`Set`が無いのに`WeakMap`や`WeakSet`がブラウザに実装されていた時代が懐かしいですね。

もうお分かりと思いますが、このMapもやはり等値比較を行います。`get`メソッドが呼ばれたとき、それと**同じ**値をキーとして登録されているデータを探さなければいけませんからね。

さっさと答えに行きましょう。このときの比較はさっき紹介した**SameValueZero**です。NaNもキーにできて安心ですね。

```js
var map = new Map();

map.set(NaN, 'I am NaN');
map.set(0, 'I am zero');

console.log(map.get(NaN)); // "I am NaN"
console.log(map.get(-0)); // "I am zero"
```

JavaScriptで等値比較の方法が気になる場面はこれくらいのはずです。

# まとめ
ということで、冒頭で紹介した`==`, `===`, `Object.is`の他に実はもう1種類の等値比較、SameValueZeroがあることが明らかになりました。表でまとめ直してみます。

|   | `==` | `===` | SameValueZero | `Object.is` |
|---|----------|-------|---------------|-------------|
| 暗黙の型変換 | 行う | 行わない | 行わない | 行わない |
| `NaN`と`NaN` | 等しくない | 等しくない | 等しい | 等しい |
| `+0`と`-0` | 等しい | 等しい | 等しい | 等しくない |

また、暗黙に等値比較が行われる場面で使われる等値比較の種類は以下の通りでした。

| 場面 | 等値比較 |
|---|---|
| `switch`文 | `===` |
| `Array#indexOf`, `Array#lastIndexOf` | `===` |
| `Array#includes` | SameValueZero |
| `Map`系 | SameValueZero |

これを見ると、なんか昔は全部`===`で頑張ってたけどES2015のときにSameValueZeroとかいう便利なやつ追加したんでそれを使うようにしましたみたいな経緯が感じられますね。特に、`Array#indexOf`とかで`NaN`が検出できないのはやはり場合によってはつらいものがありした。

逆に、`Object.is`相当の比較は全然使われていませんね。やはり、`+0`と`-0`が区別されるのはバグの元という考えのようです。それでも異なる値である以上区別する方法が無いと困るということで追加されたのが`Object.is`でしょう。

皆さんも普段何気なく行っている等値比較ですが、裏にはこのような事情があるということに思いを馳せつつコードを書いてみてはいかがでしょうか。

# 関連記事

- [JavaScriptのプリミティブへの変換を完全に理解する](https://qiita.com/uhyo/items/44c2f79873de13186743) 本文中でも紹介しましたが、この記事であまり触れなかった`==`の挙動（暗黙の型変換）について詳しく述べた記事です。