Vergleiche auf Gleichheit und deren Verwendung

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

A === B

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.

Sameness Comparisons
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:

- (unary negation)

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 is 0 (or computes to 0), a -0 is introduced at that place and propogates out into stoppingForce.

Math.atan2
Math.ceil
Math.pow
Math.round
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., using Math.pow to raise -Infinity to the power of any negative, odd exponent evaluates to -0. Refer to the documentation for the individual methods.
Math.floor
Math.max
Math.min
Math.sin
Math.sqrt
Math.tan
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., both Object.is(~~(-0), -0) and Object.is(-0 << 2 >> 2, -0) evaluate to false.

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.

Siehe auch

Schlagwörter des Dokuments und Mitwirkende

 Mitwirkende an dieser Seite: fscholz, spiegelp
 Zuletzt aktualisiert von: spiegelp,