この記事は編集レビューを必要としています。ぜひご協力ください。
ES2015 には、4 種類の等価性アルゴリズムがあります:
- 抽象的な等価性比較 (Abstract Equality Comparison) (
==) - 厳格な等価性比較 (Strict Equality Comparison) (
===):Array.prototype.indexOf、Array.prototype.lastIndexOf、caseの照合で使用されます - SameValueZero:
%TypedArray%とArrayBufferコンストラクタ、MapとSetの操作、String.prototype.includes(ES2016) で使用されます - SameValue: 上記以外のすべての状況で使用されます
JavaScript には、3 種類の値比較演算子があります:
- === を使用する 厳格な等価性 (strict equality) ("三重等号" または "同一性 (identity)")
- == を使用する 寛容な等価性 (loose equality) ("二重等号")
Object.is(ECMAScript 2015 の新機能)
どの演算子を使用するかは、どのような比較を行いたいかに依存します。
手短に言えば、二重等号は 2 つの値を比較する際に型変換を実行します。一方、三重等号は型変換をせずに同じ比較を実行します (型が異なる場合は常に false を返します)。また、Object.is の動作は三重等号と同じですが、NaN、-0、+0 の扱いが特殊です。-0 と +0 は異なる値になり、Object.is(NaN, NaN) は true になります (IEEE 754 で指定されているため、通常は NaN と NaN を比較すると (二重等号、三重等号のどちらでも) false になります)。これらの違いは、プリミティブ値の扱いに関することだという点に注意してください。いずれも、パラメータが構造的・概念的に似ているかは比較しません。非プリミティブなオブジェクト x と y は、 構造が同じでも別個のオブジェクトである限り、 上記のいずれの形式でも false になります。
=== による厳格な等価性
厳格な等価性は、2 つの値が等しいか比較します。比較対象の値はどちらも、比較する前に別の値へ暗黙的に変換されることはありません。値の型が異なる場合、それらの値は等しくないとみなします。値の型が同じで数値ではない場合、同じ値であれば等しいとみなします。また、どちらの値も数値である場合は、どちらも NaN ではなく同じ値である、あるいは一方が +0 かつもう一方が -0 であるときに等しいとみなします。
var num = 0;
var obj = new String("0");
var str = "0";
var b = false;
console.log(num === num); // true
console.log(obj === obj); // true
console.log(str === str); // true
console.log(num === obj); // false
console.log(num === str); // false
console.log(obj === str); // false
console.log(null === undefined); // false
console.log(obj === null); // false
console.log(obj === undefined); // false
厳密な等価性は、たいていの使い方で正しい等価演算になります。数値以外のあらゆる値において、これは「値が自分自身と等しい」という明快な方式を用います。数値においては、少し異なった方式になっており、二つの特別な場合を考慮しています。一つは、浮動小数点数には正負両方の 0 が存在することです。これは、ある種の数学的な解答を表すために役立ちますが、ほとんどの場合は +0 と -0 の違いを意識しないため、厳格な等価性ではこれらを同じ値として扱います。もう一つは、浮動小数点数には非数 NaN の概念があることです。これは数学的に明確ではない問題、例えば正の無限大に負の無限大を加算する等への回答を表すものです。厳格な等価性では NaN を他のどの値 (自分自身も含む) とも等しくないものとして扱います。(x が NaN である場合に、(x !== x) が true になるだけです)
== による寛容な等価性
寛容な等価性は、双方の値を共通の型に変換した後で、2 つの値が等しいか比較します。(一方あるいは双方が変換される) 変換処理後に、=== と同じ方法で等価性を比較します。寛容な等価性は対称的であり、任意の値 A および B について、A == B と B == A の意味は常に同一です (変換処理を適用する順序を除く)。
さまざまな型に対して、比較演算は以下のように行います:
| オペランド B | |||||||
|---|---|---|---|---|---|---|---|
| Undefined | Null | Number | String | Boolean | Object | ||
| オペランド A | Undefined | true |
true |
false |
false |
false |
false |
| Null | true |
true |
false |
false |
false |
false |
|
| Number | false |
false |
A === B |
A === ToNumber(B) |
A === ToNumber(B) |
A == ToPrimitive(B) |
|
| String | false |
false |
ToNumber(A) === B |
A === B |
ToNumber(A) === ToNumber(B) |
A == ToPrimitive(B) |
|
| Boolean | false |
false |
ToNumber(A) === B |
ToNumber(A) === ToNumber(B) |
A === B |
ToNumber(A) == ToPrimitive(B) |
|
| Object | false |
false |
ToPrimitive(A) == B |
ToPrimitive(A) == B |
ToPrimitive(A) == ToNumber(B) |
A === B |
|
上の表で ToNumber(A) は、比較前に引数を数値に変換しようとします。これは +A (単項演算子 +) と同じ動作です。ToPrimitive(A) は、オブジェクト A の A.toString メソッドおよび A.valueOf メソッドによる変換処理を行って、オブジェクトからプリミティブ値への変換を試みます。
伝統的にも、また ECMAScript によっても、すべてのオブジェクトは undefined や null に対して寛容な等価性が成り立たないとされています。しかし、ほとんどのブラウザは、ごく一部のオブジェクト (特にあらゆるページの document.all オブジェクト) に対しては、特定の状況において値 undefined をエミュレートすることを認めています。寛容な等価性もこれに該当します。A が undefined をエミュレートするオブジェクトである場合に限り、null == A および undefined == A は true になります。それ以外のオブジェクトはすべて、undefined および null と寛容な等価性がありません。
var num = 0;
var obj = new String("0");
var str = "0";
var b = false;
console.log(num == num); // true
console.log(obj == obj); // true
console.log(str == str); // true
console.log(num == obj); // true
console.log(num == str); // true
console.log(obj == str); // true
console.log(null == undefined); // true
// 特殊なケースを除き、どちらも false
console.log(obj == null);
console.log(obj == undefined);
寛容な等価性を使用することはよい考えでないと考える開発者もいます。厳格な等価性による比較は結果が容易に予測でき、また、型変換がないため評価が早く行われます。
Same-value 等価性
最後に示す用法はSame-value 等価性です。これは、すべての状況で 2 つの値が機能的に同一かを判断します(この用法はリスコフの置換原則の実践例と言えます)。 実例として、イミュータブルなプロパティを変化させようとした場合を見てみましょう:
// Number コンストラクタに immutable な NEGATIVE_ZERO プロパティを追加
Object.defineProperty(Number, "NEGATIVE_ZERO",
{ value: -0, writable: false, configurable: false, enumerable: false });
function attemptMutation(v)
{
Object.defineProperty(Number, "NEGATIVE_ZERO", { value: v });
}
イミュータブルなプロパティを変更しようとする操作が実際の変更を伴う場合、 Object.defineProperty は例外を発生させます。しかし、実際の変更が伴わない場合は、Object.defineProperty は何もしません。v が -0 であれば変更を要求されていないので、エラーは発生しません。しかし v が +0 であれば、Number.NEGATIVE_ZERO のイミュータブルな値を変更しようとすることになります。内部的には、イミュータブルなプロパティが再定義された場合、新たに指定された値と現在の値が Same-value 等価性によって比較されます。
Same-value 等価性は Object.is メソッドによって提供されます。
Same-value-zero 等価性
Same-value 等価性に似ていますが、 +0 と -0 は等しいとみなします。
仕様書における抽象的な等価性、厳格な等価性、Same value
ES5 では、== で実行する比較を Section 11.9.3, The Abstract Equality Algorithm で説明しています。また、=== の比較は 11.9.6, The Strict Equality Algorithm で説明しています (リンク先をご覧ください。簡単かつ読みやすくなっています。ヒント: 厳格な等価性のアルゴリズムを始めにご覧ください)。 また ES5 の Section 9.12, The SameValue Algorithm では、JS エンジン内部で使用する SameValue について説明しています。大部分は厳格な等価性のアルゴリズムと同じですが、Number を扱う 11.9.6.4 および 9.12.4 が異なります。ES2015 では、このアルゴリズムを Object.is で公開するよう提案しています。
二重等号と三重等号について、11.9.6.1 で最初に行う型の確認を除けば、厳格な等価性のアルゴリズムは寛容な等価性のアルゴリズムのサブセットと考えることができます。これは、11.9.6.2 から 7 が 11.9.3.1.a から f に対応するためです。
等価性の比較を理解するためのモデル?
ES2015 より前は、二重等号と三重等号について、一方は他方を "拡張した" ものであると聞いていたかもしれません。例えば、二重等号は三重等号と同じことをすべて行うだけでなくオペランドの型変換も行うことから、三重等号を拡張したものであると聞いたことがあるかもしれません。例えば、6 == "6" です (あるいは二重等号が基本形であり、三重等号は 2 つのオペランドが同一の型であることを要求するという制約を加えていることから、三重等号が拡張形であると言われたかもしれません。どちらが理解に適したモデルであるかは、どのような見方を選ぶかによって変わります)。
しかし内蔵の等価演算子に関するこの考え方は、その "連続体" に ES2015 の Object.is を含められるように広げることが可能なモデルではありません。Object.is は二重等号より単純に "寛容" ではなく、また三重等号より "厳格" でもなく、さらに両者の中間のどこにも置けません (すなわち二重等号より厳格でも、三重等号より寛容でもありません)。同一性を比較した以下の表から、Object.is が NaN を扱う方法が原因であることがわかります。Object.is(NaN, NaN) が false に評価されるのであれば、-0 と +0 を区別することにより、三重等号より厳格であることから寛容/厳格の連続体に含めることができることに注目してください。しかし NaN の扱いは、これが虚偽であることを表します。残念ながら、Object.is は等価演算子に関する寛容さや厳格さではなく、単純に固有の特性の観点から考えなければなりません。
| x | y | == |
=== |
Object.is |
|---|---|---|---|---|
undefined |
undefined |
true |
true |
true |
null |
null |
true |
true |
true |
true |
true |
true |
true |
true |
false |
false |
true |
true |
true |
"foo" |
"foo" |
true |
true |
true |
{ foo: "bar" } |
x |
true |
true |
true |
0 |
0 |
true |
true |
true |
+0 |
-0 |
true |
true |
false |
0 |
false |
true |
false |
false |
"" |
false |
true |
false |
false |
"" |
0 |
true |
false |
false |
"0" |
0 |
true |
false |
false |
"17" |
17 |
true |
false |
false |
[1,2] |
"1,2" |
true |
false |
false |
new String("foo") |
"foo" |
true |
false |
false |
null |
undefined |
true |
false |
false |
null |
false |
false |
false |
false |
undefined |
false |
false |
false |
false |
{ foo: "bar" } |
{ foo: "bar" } |
false |
false |
false |
new String("foo") |
new String("foo") |
false |
false |
false |
0 |
null |
false |
false |
false |
0 |
NaN |
false |
false |
false |
"foo" |
NaN |
false |
false |
false |
NaN |
NaN |
false |
false |
true |
Object.is と三重等号の使いどころ
NaN の扱いは別として、一般的に、 Object.is のゼロに対する特別な動作が関心の対象になりえると思われるのは、ある種のメタプログラミング方式に則る時、特にプロパティ記述子に関して Object.defineProperty の特徴の一部を再現したい時に限られます。このような要件が必要なければ、 Object.is ではなく、代わりに === を使用してはいかがでしょう。2 つの NaN 値を比較した結果が true になることが必要な場合であっても、通常は、 NaN をチェックして特別扱いする方が (前バージョンの ECMAScript からは isNaN メソッドを使えます) 、比較処理中に現れた全てのゼロについてその符号が周囲の処理からどう影響されるのか悩むよりも簡単です。
すべてを網羅してはいませんが、-0 と +0 の区別が発生する可能性がある内蔵メソッドや演算子を以下に示します。コード中ではこれらを考慮して下さい:
- (単項否定演算子)-
0を否定すると-0になることは明白です。しかし式の抽象化が、知らないうちに-0を取り込ませてしまう可能性があります。例えば、以下のコードについて考えてみましょう:let stoppingForce = obj.mass * -obj.velocity
obj.velocityが0である (あるいは計算結果が0になる) とき、そこで-0が生成されてstoppingForceに伝播します。
Math.atan2Math.ceilMath.powMath.round- 引数に
-0が存在しなくても、場合によってはこれらのメソッドの戻り値として-0が式に取り込まれる可能性があります。例えば、負の値の累乗で-Infinityが発生するようにMath.powを使用したとき、奇数の指数は-0に評価されます。それぞれのメソッドのドキュメントを確認してください。
Math.floorMath.maxMath.minMath.sinMath.sqrtMath.tan- 引数のひとつが
-0である場合に、これらのメソッドから-0を戻り値として得る可能性があります。例えば、Math.min(-0, +0)は-0になります。それぞれのメソッドのドキュメントを確認してください。
~<<>>- これらの演算子は、内部で ToInt32 アルゴリズムを使用します。内部の 32 ビット整数型は 0 の表現が 1 種類しかないため、逆の演算を行った後に
-0は戻らないでしょう。例えばObject.is(~~(-0), -0)やObject.is(-0 << 2 >> 2, -0)は、falseになります。
ゼロの符号を考慮していない場合に、Object.is に頼ることは危険でしょう。もちろん -0 と +0 を区別する意図があれば、これはまさに望むことです。