JavaScript のイコール
- カテゴリ:
- JavaScript
- コメント数:
- Comments: 0
◆ ==: 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
◆ ===: Strict Equality Comparison
◆ 0/-0 は true で NaN どうしは false
◆ Object.is: SameValue
◆ 0/-0 は false で NaN どうしは true
◆ 内部用: SameValueZero
◆ 0/-0 は true で NaN どうしは true
この記事に追記したついでに JavaScript のイコールを調べてみました
== は Abstract Equality Comparison
=== は Strict Equality Comparison
というちゃんとした呼び方があります
知っての通り === は型も一致してないといけないです
== は自動で変換してくれた上で比較します
変換規則も覚えておかないと [] は
なのに
になったりと困ることが多いです
なので null または undefined という意味で 「== null」 を使う時以外はすべて === が良いと言われています
数字と文字列を一緒にして扱うのは気持ち悪いですしバグのもとなので比較するならその場にふさわしい方にキャストすべきです
キャストは演算子でできるので (int) みたいなことを書く言語よりも簡単です
また switch の case など 内部で比較してくれるところで == が使われるところはないです
こういった比較の時に 型が違うせいで なぜか一致しない というのをなくすように 目的の型に合わせて普段からキャストするようにしておくと安心です
そんなわけで == はほぼ要らないもので 複雑な 「○型と×型なら何型変換される」 を全パターン覚える必要もないと思います
といっても PHP みたいな変な変換はせず素直なものなので あまり苦労もしないと思います
ただ 覚えていたとしても === になってるとコードみただけで 「この変数は数値型が入ってるはず」 みたいに期待する型がわかるので === のほうが見やすいコードになると思います
関数ですが 比較するためのものです
Object.is は SameValue という呼ばれる比較方法です
=== は型も見るのに 直感的じゃない 見た目と違う動きがするところが 2 つありました
そこを Object.is では 見た目通りに同じ値はイコールにする動きになっています
SameValue という言葉通り 「同じ値」 であるかをチェックするので -0 と 0 は別物 NaN と NaN は同じものとなっています
=== と Object.is の違いはこれだけです
逆に言うと === はここだけが見た目と違う比較しているわけです
SameValue から 0 と -0 を同じ値として扱うものです
=== と比べると NaN どうしを同じ値とみなしたものです
これは内部で使われるだけで 演算子や関数は用意されていません
includes では NaN どうしが true で 0/-0 の区別がないので SameValueZero です
indexOf では NaN どうしが false で 0/-0 の区別もないので === です
NaN どうしが false で 0/-0 が一緒なので === です
NaN どうしは一緒で 0/-0 も一緒なので SameValueZero です
ですが どう使われてるのかわからなかったので Spec 見てみました
[TypedArray]
[ArrayBuffer]
こういう使われ方のようです
どちらも引数が 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
んー
前提知識ないので全くわからない です
とりあえず 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 として初期化されるよー くらいですし 気にするほどじゃなさそうですけど
--(/追記)--
Array.prototype.includes の間違いですね
文字列の検索に関係ない比較ですし String の includes は ES2015 で追加されていて ES2016 で追加されるのは Array の includes です
一応 Spec を検索しても TypedArray/ArrayBuffer/Map/Set/Array.prototype.includes しかでてこないです
ES2016
== と ===
昔の方法では == と === の 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
// 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
なのに
[] == true
// false
// false
になったりと困ることが多いです
なので null または undefined という意味で 「== null」 を使う時以外はすべて === が良いと言われています
数字と文字列を一緒にして扱うのは気持ち悪いですしバグのもとなので比較するならその場にふさわしい方にキャストすべきです
var a = 1
var b = "1"
a === +b
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
// 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
// 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
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
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.
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.
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;
}
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
ES2015ES2016