厳密等価演算子(===
)は2つの値が等しいかどうかを判定する演算子だ。
===
は2つの値が等しい場合にtrue
を返し、等しくない場合にfalse
を返す。
通常の等価演算子(==
、!=
)が比較の際に左右のオペランドの型を自動的に比較可能なものへ変換するのに対し、こちらの厳密等価演算子は比較の際に型の変換をおこなわない。
「等しくない」かどうかを判定したい場合は!==
を使用する。
!==
は2つの値が等しくない場合にtrue
を返し、等しい場合にfalse
を返す。
このMDNのページではその仕様について以下のように説明している。
ここに書かれていることを正しく理解すれば誤解は生じないのだが、一部直感に反するような仕様があるため誤った理解をしている人もしばしば見受けられる。 以下では勘違いしやすい点やわかりにくい点について重点的に解説していきたい。
解説の前に厳密等価演算子を使用した比較の簡単な例を示す。
中には「なぜこれがこの結果になるのか」と疑問に感じるものがあるかもしれない。 そう感じた場合は厳密等価演算子の仕様について誤解している可能性が高い。 今一度正しい仕様を確認する必要があるだろう。
123 === 123; // true
"123" === "123"; // true
123 === Number("123"); // true
true === true; // true
null === null; // true
undefined === undefined; // true
let a = { value: 123 };
let b = { value: 123 };
a === a; // true
b === b; // true
123 === "123"; // false
123 === new Number(123); // false
0 === true; // false
1 === true; // false
"" === false; // false
null === undefined; // false
NaN === NaN; // false
let a = { value: 123 };
let b = { value: 123 };
a === b; // false
通常の等価演算子ではtrue
と判定されていた組み合わせも、厳密等価演算子では型が異なる時点で全てfalse
と判定される。
let a = { value: 123 };
let b = { value: 123 };
a === a; // true
b === b; // true
a === b; // false
オブジェクト同士の比較ではオブジェクトそのものが同じものであるかどうかを判定するため、中身が全く同じだとしてもオブジェクトそのものが異なる場合は「等しくない」と判定される。
よくオブジェクトは入れ物と中身に例えられる。
上の例で言えば、a
とb
が持つ値はどの入れ物かという情報であり、それぞれの入れ物の中に{ value: 123 }
という中身が入っている状態だ。
ここでa === b
という比較は入れ物が同じかどうかを比較しているため、異なる入れ物であると判定されているわけだ。
それを踏まえた上で次の例を見てもらいたい。
let a = { value: 123 };
let b = { value: 123 };
let c = a;
a === c; // true ※ a と c は入れ物が同じ
b === c; // false ※ b と c は入れ物が異なる
このようにa
の値をc
に代入した場合、a
とc
は同じ入れ物を指すこととなり等しいと判定される。
1 === 1; // true
1 === new Number(1); // false
"ABC" === "ABC"; // true
"ABC" === new String("ABC"); // false
true === true; // true
true === new Boolean(true); // false
JavaScriptでは数値や文字列などのプリミティブ型に対応するオブジェクトラッパー型が定義されているものがある。
文字列ならString
型、数値ならNumber
型、長整数ならBigInt
型、論理値ならBoolean
型といったものだ。
これらはプリミティブ型とオブジェクトラッパー型で同じ値を表現することはできるが、厳密等価演算子で比較すると型が違うため「等しくない」と判定される。
let a = new Number(1);
let b = new Number(1);
let c = a;
a === a; // true
b === b; // true
a === b; // false
a === c; // true
b === c; // false
オブジェクトラッパー型を使用して数値や文字列を表すこともできるが、それらを厳密等価演算子で比較すると単にオブジェクトとして比較されてしまう。 前項で説明した通り、オブジェクト同士の比較はオブジェクトそのものが同じかどうかで判定される。
function Test(key)
{
if (key === "12345")
return "OK";
return "FAILED";
}
let key = new String("12345");
let result = Test(key); // result : "FAILED"
この仕様を正しく理解していないとこのような失敗を招く可能性もある。
Test()
関数の中では与えられたkey
がプリミティブ型の文字列である"12345"
と等しいかどうかを判定している。
しかしその関数を呼び出す際に与えられた引数がもしオブジェクトラッパー型のnew String("12345")
だった場合、それはプリミティブ型の"12345"
とは異なるものと判定される。
少し本題からは逸れるが、ここでオブジェクトラッパー型のコンストラクタと変換関数について触れておきたい。
以下は数値におけるNumber
型のコンストラクタとNumber()
関数で説明するが、文字列や論理値についても同様だ。
オブジェクトラッパー型のコンストラクタは次のようなものだ。
let a = new Number(123); // 123
let b = new Number("500"); // 500
let c = new Number(true); // 1
let d = new Number("abc"); // NaN
引数に与えられた値をNumber
型のオブジェクトに変換することができる。
数値に変換できない値が渡された場合はNaN
が返される。
次に変換関数の例を示す。
let a = Number(123); // 123
let b = Number("500"); // 500
let c = Number(true); // 1
let d = Number("abc"); // NaN
こちらも引数に与えられた値を数値に変換するが、返されるのはNumber
型のオブジェクトではなくプリミティブ型の数値だ。
この違いによって、これらの値を厳密等価演算子で比較する際は以下のような結果になる。
1 === Number(1); // true
1 === new Number(1); // false
Number(1) === new Number(1); // false
1 === Number(new Number(1)); // true
Number(1) === Number(new Number(1)); // true
new Number(1) === new Number(1); // false
比較対象がいずれもプリミティブ型の場合は直感に沿った結果となるが、いずれかまたは両方がオブジェクト型の場合は注意が必要だ。
NaN
(Not-A-Number)は数値ではない状態を表す特殊な値だ。
このNaN
を別の値と比較した場合、対象の値がなんであれ常に「等しくない」と判定される。
これはNaN
同士を比較した場合も等しくないと判定されるということだ。
1 === NaN; // false
"A" === NaN; // false
NaN === NaN; // false ※ここに注意
逆に!==
で「等しくないかどうか」を判定する場合は常に結果はtrue
になるということだ。
1 !== NaN; // true
"A" !== NaN; // true
NaN !== NaN; // true ※ここに注意
この仕様については厳密等価演算子だけでなく、通常の等価演算子でも同様となっている。 少々癖のある仕様なのでしっかりと把握しておきたい。