JavaScript bietet drei verschiedene Operationen an, um Werte zu vergleichen:
- strikte Gleichheit (oder "triple equals" or "identity") mit ===,
- lose Gleichheit ("double equals") mit ==,
- und
Object.is
(neu in ECMAScript 6).
Die Wahl der Operation hängt von der Art des gewünschten Vergleichs auf Gleichheit ab.
Kurz gesagt nimmt double equals eine Typkonvertierung der Operanden vor, bevor der Vergleich der Werte gemacht wird. Bei triple equals werden die Werte ohne vorherige Typkonvertierung miteinander verglichen. Wenn sich die Datentypen der beiden Operanden unterscheiden liefert triple equals immer false
zurück. Object.is
verhält sich wie triple equals und bietet darüber hinaus eine spezielle Behandlung für NaN
und -0
und +0
an. -0
und +0
sind für Object.is
ungleich während Object.is(NaN, NaN) true
ist. Laut IEEE 754 ergibt ein Vergleich von zwei NaN
mit double equals oder triple equals false
. Diese drei Operationen unterscheiden sich ihrere Behandlung von primitiven Datentypen. Es wird nicht geprüft, ob die beiden Operanden konzeptionell diesselbe Struktur besitzen. Für die nichtprimitiven Objekte x und y, welche diesselbe Struktur besitzen aber zwei unterschiedliche Objekte sind, ergeben die drei Operationen false
.
Strikte Gleichheit mit ===
Strikte Gleichheit prüft zwei Werte auf Gleichheit. Keiner der Werte wird vor dem Vergleich implizit konvertiert. Wenn die Werte verschiedene Datentypen haben, werden die Werte als ungleich betrachtet. Anderenfalls, wenn die Werte denselben Datentyp haben und keine Zahlen sind, werden sie als gleich betrachtet, solange sie denselben Wert haben. Wenn beide Werte Zahlen sind, werden sie als gleich betrachtet, solange beide nicht NaN sind und denselben Wert haben oder der eine Wert +0 und der andere Wert -0 ist.
var num = 0; var obj = new String("0"); var str = "0"; var b = false; print(num === num); // true print(obj === obj); // true print(str === str); // true print(num === obj); // false print(num === str); // false print(obj === str); // false print(null === undefined); // false print(obj === null); // false print(obj === undefined); // false
Strikte Gleichheit ist fast immer die am meisten geeignete Vergleichsoperation. Für alle Werte, die keine Zahlen sind, verwendet sie die naheliegende Semantik: ein Wert ist nur mit sich selbst gleich. Für Zahlen kommt eine leicht unterschiedliche Semantik zum Einsatz, da zwei Grenzfälle berücksichtigt werden müssen. Im ersten Grenzfall kann die Zahl 0 als Gleitkommazahl ein positives oder negatives Vorzeichen haben. Dies kann zur Repräsentation von bestimmten mathematischen Lösungen nützlich sein. Da aber in den meisten Situationen nicht zwischen +0 und -0 unterschieden wird, behandelt die strikte Gleichheit diese zwei Werte als gleich. Der zweite Grenzfall ergibt sich dadruch, dass Gleitkommazahlen einen keine-Zahl Wert haben, NaN
(not-a-number). Dadurch können Lösungen für schlecht definierte mathematische Probleme dargestellt werden (z.B.: negativ unendlich plus positiv undendlich). Strikte Gleichheit behandelt NaN
als ungleich zu jedem anderen Wert und sich selbst. Der einzige Fall, in dem (x !== x)
true
ergibt, ist, wenn x
den Wert NaN
hat.
Lose Gleichheit mit ==
Lose Gleichheit vergleicht zwei Werte auf deren Gleichheit, nachdem beide zu demselben Datentyp konvertiert wurden. Nach der Konvertierung (ein oder beide Werte können konvertiert werden) wird der finale Vergleich wie bei ===
ausgeführt. Lose Gleichheit ist symmetrisch: A == B
hat immer dieselbe Semantik wie B == A
für alle Werte von A
und B
.
Der Vergleich auf Gleichheit wird wie folgt für Operanden mit den verschiedenen Datentypen ausgeführt:
Operand B | |||||||
---|---|---|---|---|---|---|---|
Undefined | Null | Number | String | Boolean | Object | ||
Operand A | Undefined | true |
true |
false |
false |
false |
IsFalsy(B) |
Null | true |
true |
false |
false |
false |
IsFalsy(B) |
|
Number | false |
false |
A === B |
A === ToNumber(B) |
ToNumber(B) === A |
ToPrimitive(B) == A |
|
String | false |
false |
B === ToNumber(A) |
A === B |
ToNumber(A) === ToNumber(B) |
ToPrimitive(B) == A |
|
Boolean | false |
false |
ToNumber(A) === B |
ToNumber(A) === ToNumber(B) |
A === B |
false |
|
Object | IsFalsy(A) |
IsFalsy(A) |
ToPrimitive(A) == B |
ToPrimitive(A) == B |
false |
|
In der oberen Tabelle versucht ToNumber(A)
sein Argument vor dem Vergleich in eine Zahl zu konvertieren. Das Verhalten ist äquivalent zu +A
(der unäre + Operator). ToPrimitive(A)
versucht sein Argument, das ein Objekt ist, in einen primitiven Wert zu konvertieren. Dazu wird eine unterschiedliche Sequenz von A.toString
und A.valueOf
Methoden von A
aufzurufen.
Traditionell und laut ECMAScript sind alle Objekte lose ungleich zu undefined
und null
. Aber die meisten Webbbrowser erlauben einer sehr kleinen Menge von Objekten (speziell das document.all
Objekt für jede Seite), dass sie sich in bestimmten Kontexten so verhalten, als ob sie den Wert undefined
emulieren. Lose Gleichheit ist ein derartiger Kontext. Daher ergibt die Methode IsFalsy(A)
genau dann true
, wenn A ein Objekt ist, das undefined
emuliert. In allen anderen Fällen ist ein Objekt nie lose gleich zu undefined
oder null
.
var num = 0; var obj = new String("0"); var str = "0"; var b = false; print(num == num); // true print(obj == obj); // true print(str == str); // true print(num == obj); // true print(num == str); // true print(obj == str); // true print(null == undefined); // true // both false, except in rare cases print(obj == null); print(obj == undefined);
Manche Entwickler haben die Ansicht, dass die Verwendung der losen Gleichheit fast nie eine gute Idee ist. Das Resultat des Vergleichs mit strikter Gleichheit ist einfacher vorherzusagen und die Auswertung ist schneller, da keine Konvertierung der Werte stattfindet.
Same-value Gleichheit
Same-value Gleichheit adressiert den dritten Fall: Bestimmung, ob zwei Werte in allen Kontexten funktional identisch sind. Dieser Anwendungsfall demonstriert eine Instanz des Liskovschen Substitutionsprinzip. Eine Instanz tritt auf, wenn versucht wird ein nicht veränderbares Property zu verändern:
// Add an immutable NEGATIVE_ZERO property to the Number constructor. 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
wird eine Exception werfen, wenn ein Versuch zum Verändern eines unveränderbares Property es verändern würde. Es passiert nichts, solange keine Veränderung stattfinden soll. Wenn v
-0
ist, wurde keine Veränderung angefragt und somit wird keine Exception geworfen. Wenn v
aber +0
ist, hätte Number.NEGATIVE_ZERO
nicht länger seinen unveränderbaren Wert. Wenn ein unveränderbares Property neudefiniert wird, wird der neu spezifizierte Wert intern mittels der Same-value Gleichheit mit dem aktuellen Wert verglichen.
Die Same-value Gleichheit wird von der Object.is
Methode angeboten.
Abstrakte Gleichheit, strikte Gleichheit und same-value Gleichheit in der Spezifikation
In ECMAScript 5 wird der Vergleich mit ==
in Section 11.9.3, The Abstract Equality Algorithm beschrieben. Der ===
Vergleich ist in 11.9.6, The Strict Equality Algorithm zu finden. (Diese beiden Abschnitte sind kurz und verständlich. Hinweis: zuerst den Abschnitt Strict Equality Algorithm lesen) ECMAScript 5 beschreibt auch die same-value Gleichheit in Section 9.12, The SameValue Algorithm für die interne Verwendung in der JavaScript Engine. Dieser Abschnitt ist hauptsächlich derselbe wie Strict Equality Algorithm mit der Ausnahme, dass sich 11.9.6.4 und 9.12.4 in der Behandlung von Zahlen (Number
) unterscheiden. ECMAScript 6 schlägt vor, dass dieser Algorithmus über Object.is
angeboten wird.
Wir können erkennen, dass mit double und triple equals, mit der Ausnahme der vorhergehenden Typkonvertierung in 11.9.6.1, der Strict Equality Algorithm eine Teilmenge des Abstract Equality Algorithm ist, weil 11.9.6.2–7 dem Abschnitt 11.9.3.1.a–f entspricht.
A model for understanding equality comparisons?
Prior to ES6, you might have said of double equals and triple equals that one is an "enhanced" version of the other. For example, someone might say that double equals is an extended version of triple equals, because the former does everything that the latter does, but with type conversion on its operands. E.g., 6 == "6"
. (Alternatively, someone might say that double equals is the baseline, and triple equals is an enhanced version, because it requires the two operands to be the same type, so it adds an extra constraint. Which one is the better model for understanding depends on how you choose to view things.)
However, this way of thinking about the built-in sameness operators is not a model that can be stretched to allow a place for ES6's Object.is
on this "spectrum". Object.is
isn't simply "looser" than double equals or "stricter" than triple equals, nor does it fit somewhere in between (i.e., being both stricter than double equals, but looser than triple equals). We can see from the sameness comparisons table below that this is due to the way that Object.is
handles NaN
. Notice that if Object.is(NaN, NaN)
evaluated to false
, we could say that it fits on the loose/strict spectrum as an even stricter form of triple equals, one that distinguishes between -0
and +0
. The NaN
handling means this is untrue, however. Unfortunately, Object.is
simply has to be thought of in terms of its specific characteristics, rather than its looseness or strictness with regard to the equality operators.
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 |
When to use Object.is
versus triple equals
Aside from the way it treats NaN
, generally, the only time Object.is
's special behavior towards zeros is likely to be of interest is in the pursuit of certain meta-programming schemes, especially regarding property descriptors when it is desirable for your work to mirror some of the characteristics of Object.defineProperty
. If your use case does not require this, it is suggested to avoid Object.is
and use ===
instead. Even if your requirements involve having comparisons between two NaN
values evaluate to true
, generally it is easier to special-case the NaN
checks (using the isNaN
method available from previous versions of ECMAScript) than it is to work out how surrounding computations might affect the sign of any zeros you encounter in your comparison.
Here's an in-exhaustive list of built-in methods and operators that might cause a distinction between -0
and +0
to manifest itself in your code:
-
It's obvious that negating
0
produces-0
. But the abstraction of an expression can cause-0
to creep in when you don't realize it. For example, consider:let stoppingForce = obj.mass * -obj.velocity
If
obj.velocity
is0
(or computes to0
), a-0
is introduced at that place and propogates out intostoppingForce
.
- It's possible for a
-0
to be introduced into an expression as a return value of these methods in some cases, even when no-0
exists as one of the parameters. E.g., usingMath.pow
to raise-Infinity
to the power of any negative, odd exponent evaluates to-0
. Refer to the documentation for the individual methods.
- It's possible to get a
-0
return value out of these methods in some cases where a-0
exists as one of the parameters. E.g.,Math.min(-0, +0)
evalutes to-0
. Refer to the documentation for the individual methods.
~
<<
>>
- Each of these operators uses the ToInt32 algorithm internally. Since there is only one representation for 0 in the internal 32-bit integer type,
-0
will not survive a round trip after an inverse operation. E.g., bothObject.is(~~(-0), -0)
andObject.is(-0 << 2 >> 2, -0)
evaluate tofalse
.
Relying on Object.is
when the signedness of zeros is not taken into account can be hazardous. Of course, when the intent is to distinguish between -0
and +0
, it does exactly what's desired.