◆ ==: Abstract Equality Comparison
◆ ===: Strict Equality Comparison
  ◆ 0/-0 は true で NaN どうしは false
◆ Object.is: SameValue
  ◆ 0/-0 は false で NaN どうしは true
◆ 内部用: SameValueZero
  ◆ 0/-0 は true で NaN どうしは true

この記事に追記したついでに JavaScript のイコールを調べてみました

== と ===

昔の方法では == と === の 2 つが比較する方法です

== は Abstract Equality Comparison
=== は Strict Equality Comparison
というちゃんとした呼び方があります

知っての通り === は型も一致してないといけないです
== は自動で変換してくれた上で比較します
1 == 1
// true

1 == "1"
// true

0 == "0"
// true

true == "true"
// false

false == "false"
// false

"0" == true
// true

1 == [1]
// true

null == undefined
// true

false == null
// false

変換規則も覚えておかないと [] は
if([]) console.log(true)
// true

なのに
[] == true
// false

になったりと困ることが多いです

なので null または undefined という意味で 「== null」 を使う時以外はすべて === が良いと言われています

数字と文字列を一緒にして扱うのは気持ち悪いですしバグのもとなので比較するならその場にふさわしい方にキャストすべきです
var a = 1
var b = "1"
a === +b

キャストは演算子でできるので (int) みたいなことを書く言語よりも簡単です

また switch の case など 内部で比較してくれるところで == が使われるところはないです
こういった比較の時に 型が違うせいで なぜか一致しない というのをなくすように 目的の型に合わせて普段からキャストするようにしておくと安心です


そんなわけで == はほぼ要らないもので 複雑な 「○型と×型なら何型変換される」 を全パターン覚える必要もないと思います
といっても PHP みたいな変な変換はせず素直なものなので あまり苦労もしないと思います

ただ 覚えていたとしても === になってるとコードみただけで 「この変数は数値型が入ってるはず」 みたいに期待する型がわかるので === のほうが見やすいコードになると思います

=== と Object.is

ES6 からは Object.is が増えました
関数ですが 比較するためのものです

Object.is は SameValue という呼ばれる比較方法です

=== は型も見るのに 直感的じゃない 見た目と違う動きがするところが 2 つありました
そこを Object.is では 見た目通りに同じ値はイコールにする動きになっています
0 === -0
// true

Object.is(0, -0)
// false

NaN === NaN
// false

Object.is(NaN, NaN)
// true

SameValue という言葉通り 「同じ値」 であるかをチェックするので -0 と 0 は別物 NaN と NaN は同じものとなっています

=== と Object.is の違いはこれだけです
逆に言うと === はここだけが見た目と違う比較しているわけです

SameValueZero

実はもうひとつ 比較方法があります

SameValue から 0 と -0 を同じ値として扱うものです
=== と比べると NaN どうしを同じ値とみなしたものです

これは内部で使われるだけで 演算子や関数は用意されていません

つかわれるところ

配列の検索

;[0].includes(-0)
// true
;[-0].includes(0)
// true
;[NaN].includes(NaN)
// true

;[0].indexOf(0)
// 0
;[-0].indexOf(-0)
// 0
;[NaN].indexOf(NaN)
// -1

includes では NaN どうしが true で 0/-0 の区別がないので SameValueZero です
indexOf では NaN どうしが false で 0/-0 の区別もないので === です

switch

switch(0){
    case -0: console.log(true)
}
// true

switch(-0){
    case 0: console.log(true)
}
// true

switch(NaN){
    case NaN: console.log(true)
}
// undefined

NaN どうしが false で 0/-0 が一緒なので === です

Map/Set

var m = new Map()
m.set(0, 1)
m.set(-0, 2)
m.set(NaN, 3)
m.set(NaN, 4)
// Map {0 => 2, NaN => 4}

m.get(0)
// 2
m.get(-0)
// 2
m.get(NaN)
// 4

var s = new Set()
s.add(-0)
s.add(NaN)
// Set {0, NaN}

s.has(0)
// true
s.has(-0)
// true
s.has(NaN)
// true

NaN どうしは一緒で 0/-0 も一緒なので SameValueZero です

TypedArray と ArrayBuffer

TypedArray と ArrayBuffer のコンストラクタでは SameValueZero がつかわれてるようです
ですが どう使われてるのかわからなかったので Spec 見てみました


[TypedArray]
Let numberLength be ? ToNumber(length).
Let elementLength be ToLength(numberLength).
If SameValueZero(numberLength, elementLength) is false, throw a RangeError exception.

[ArrayBuffer]
Let numberLength be ? ToNumber(length).
Let byteLength be ToLength(numberLength).
If SameValueZero(numberLength, byteLength) is false, throw a RangeError exception.

こういう使われ方のようです

どちらも引数が length なので length を ToNumber したものと ToNumber したあとにさらに ToLength したものを SameValueZero で比較して違ったら RangeError の例外が起きるようです

ToNumber は + をつけて +a のように変換するのと一緒です
number 型はそのままで undefined が NaN になったり boolean 型だと true が 1 で false 0 になったりです

ToLength では 「If len ≤ +0, return +0」 という処理があるので -0 が 0 になります
また内部の ToInteger で NaN は +0 になります


-0 が引数の時に SameValueZero(-0, 0) になるので 比較方法が SameValue だと異なってエラーになるからこの比較方法になってるのだと思います

でも NaN の場合は ToLength したら 0 になって SameValueZero(NaN, 0) で値違うのにエラーが起きていないんです

よくわからない
実装が仕様通りじゃないのかな

仕様に準拠してそうな Firefox を見てみると Uint8Array など TypedArray では NaN や undefined 入れるとエラーでした
undefined で TypeError なのは仕様どおりですが NaN でも TypeError で RangeError ではありません

この 2 つに関してはよくわからないですが 比較方法がなんでも困る気がしないので気にしないことにします



--(追記)--

v8 のソースを見ることがあったので ArrayBuffer のコンストラクタを見てみました

今の Chrome のバージョンの 5.6.326 です
https://github.com/v8/v8/blob/5.6.326/src/builtins/builtins-arraybuffer.cc
// ES6 section 24.1.2.1 ArrayBuffer ( length ) for the [[Construct]] case.
BUILTIN(ArrayBufferConstructor_ConstructStub) {
  HandleScope scope(isolate);
  Handle<JSFunction> target = args.target();
  Handle<JSReceiver> new_target = Handle<JSReceiver>::cast(args.new_target());
  Handle<Object> length = args.atOrUndefined(isolate, 1);
  DCHECK(*target == target->native_context()->array_buffer_fun() ||
         *target == target->native_context()->shared_array_buffer_fun());
  Handle<Object> number_length;
  ASSIGN_RETURN_FAILURE_ON_EXCEPTION(isolate, number_length,
                                     Object::ToInteger(isolate, length));
  if (number_length->Number() < 0.0) {
    THROW_NEW_ERROR_RETURN_FAILURE(
        isolate, NewRangeError(MessageTemplate::kInvalidArrayBufferLength));
  }
  Handle<JSObject> result;
  ASSIGN_RETURN_FAILURE_ON_EXCEPTION(isolate, result,
                                     JSObject::New(target, new_target));
  size_t byte_length;
  if (!TryNumberToSize(*number_length, &byte_length)) {
    THROW_NEW_ERROR_RETURN_FAILURE(
        isolate, NewRangeError(MessageTemplate::kInvalidArrayBufferLength));
  }
  SharedFlag shared_flag =
      (*target == target->native_context()->array_buffer_fun())
          ? SharedFlag::kNotShared
          : SharedFlag::kShared;
  if (!JSArrayBuffer::SetupAllocatingData(Handle<JSArrayBuffer>::cast(result),
                                          isolate, byte_length, true,
                                          shared_flag)) {
    THROW_NEW_ERROR_RETURN_FAILURE(
        isolate, NewRangeError(MessageTemplate::kArrayBufferAllocationFailed));
  }
  return *result;
}

んー
前提知識ないので全くわからない です

とりあえず if で THROW_NEW_ERROR_RETURN_FAILURE がチェックしてダメならエラーにするというところみたい

int 化した number_length が 0 未満なら RangeError
size 化して byte_length に代入できなかったら RangeError

の 2 つがチェックしてるところみたい
最後のはメモリアロケーションぽいから今回のとは関係なし


仕様とはちょっと違う実装みたいです
new ArrayBuffer(-1) だとちゃんと RangeError になります

NaN や -0 が通るのは number_length->Number() で取得するのが 0 になってるからだと思います
Object::ToInterger を見てみたのですが 中で ConvertToInteger を呼び出していて これは検索してもこの 1 箇所しか使われてませんでした
外部ライブラリかな?

とりあえず ここの違いは実装が仕様どおりじゃないので 気にしなくてよさそうです
仕様通りでも -0 でも 0 として初期化されるよー くらいですし 気にするほどじゃなさそうですけど

--(/追記)--

MDN

MDN もみてみましたが MDN では 「ES2016 で追加される String.prototype.includes に SameValueZero」 と書いてありました
Array.prototype.includes の間違いですね
文字列の検索に関係ない比較ですし String の includes は ES2015 で追加されていて ES2016 で追加されるのは Array の includes です

一応 Spec を検索しても TypedArray/ArrayBuffer/Map/Set/Array.prototype.includes しかでてこないです

spec

ES2015
ES2016