Gleichheitsvergleiche und Gleichheit
JavaScript bietet drei verschiedene Methoden zum Wertevergleich:
===
— strikte Gleichheit (dreifach gleich)==
— lose Gleichheit (doppelt gleich)Object.is()
Welche Methode Sie wählen, hängt davon ab, welche Art von Vergleich Sie durchführen möchten. Kurz gesagt:
- Doppelt gleich (
==
) führt eine Typkonvertierung durch, wenn zwei Dinge verglichen werden, und behandeltNaN
,-0
und+0
speziell gemäß IEEE 754 (alsoNaN != NaN
und-0 == +0
); - Dreifach gleich (
===
) führt denselben Vergleich wie doppelt gleich durch (einschließlich der speziellen Behandlung fürNaN
,-0
und+0
), jedoch ohne Typkonvertierung; wenn die Typen unterschiedlich sind, wirdfalse
zurückgegeben. Object.is()
führt keine Typkonvertierung und keine spezielle Behandlung fürNaN
,-0
und+0
durch (was ihm dasselbe Verhalten wie===
gibt, abgesehen von diesen speziellen numerischen Werten).
Sie entsprechen drei der vier Gleichheitsalgorithmen in JavaScript:
- IsLooselyEqual:
==
- IsStrictlyEqual:
===
- SameValue:
Object.is()
- SameValueZero: wird von vielen eingebauten Operationen verwendet
Beachten Sie, dass der Unterschied zwischen diesen Methoden mit der Behandlung von Primitiva zu tun hat; keine von ihnen vergleicht, ob die Parameter konzeptionell ähnlich in ihrer Struktur sind. Für nicht-primitive Objekte x
und y
, die dieselbe Struktur haben, aber als separate Objekte existieren, wird jede der oben genannten Formen false
zurückgeben.
Strikte Gleichheit mit ===
Strikte Gleichheit vergleicht zwei Werte auf Gleichheit. Keiner der Werte wird implizit in einen anderen Wert umgewandelt, bevor der Vergleich durchgeführt wird. Wenn die Werte unterschiedliche Typen haben, werden sie als ungleich angesehen. Wenn die Werte denselben Typ haben, keine Zahlen sind und denselben Wert haben, werden sie als gleich angesehen. Schließlich, wenn beide Werte Zahlen sind, werden sie als gleich angesehen, wenn sie beide nicht NaN
sind und denselben Wert haben oder wenn eines +0
und eines -0
ist.
const num = 0;
const obj = new String("0");
const str = "0";
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
Strikte Gleichheit ist fast immer die korrekte Vergleichsoperation, die verwendet werden sollte. Für alle Werte außer Zahlen verwendet sie die offensichtlichen Semantiken: Ein Wert ist nur mit sich selbst gleich. Für Zahlen verwendet sie leicht abweichende Semantiken, um zwei verschiedene Sonderfälle zu überdecken. Der erste ist, dass das Fließkomma-Null entweder positiv oder negativ sein kann. Dies ist nützlich, um bestimmte mathematische Lösungen darzustellen, aber da die meisten Situationen keinen Unterschied zwischen +0
und -0
beachten, behandelt die strikte Gleichheit sie als denselben Wert. Der zweite Sonderfall ist, dass Fließkomma das Konzept eines Nicht-einer-Zahl-Wertes, NaN
, enthält, um die Lösung bestimmter unklar definierter mathematischer Probleme darzustellen: negative Unendlichkeit addiert zu positiver Unendlichkeit, zum Beispiel. Strikte Gleichheit behandelt NaN
als ungleich zu jedem anderen Wert – einschließlich sich selbst. (Der einzige Fall, in dem (x !== x)
true
ist, ist, wenn x
NaN
ist.)
Neben ===
wird strikte Gleichheit auch von Methoden zur Array-Indexsuche verwendet, einschließlich Array.prototype.indexOf()
, Array.prototype.lastIndexOf()
, TypedArray.prototype.indexOf()
, TypedArray.prototype.lastIndexOf()
und case
-Matching. Das heißt, Sie können indexOf(NaN)
nicht verwenden, um den Index eines NaN
Werts in einem Array zu finden, oder NaN
als case
Wert in einer switch
Anweisung verwenden und es dazu bringen, etwas zu passen.
console.log([NaN].indexOf(NaN)); // -1
switch (NaN) {
case NaN:
console.log("Surprise"); // Nothing is logged
}
Lose Gleichheit mit ==
Lose Gleichheit ist symmetrisch: A == B
hat immer dieselbe Semantik wie B == A
für beliebige Werte von A
und B
(abgesehen von der Reihenfolge der angewandten Konvertierungen). Das Verhalten für die Ausführung loser Gleichheit unter Verwendung von ==
ist wie folgt:
- Wenn die Operanden denselben Typ haben, werden sie wie folgt verglichen:
- Objekt: gibt
true
nur zurück, wenn beide Operanden auf dasselbe Objekt verweisen. - String: gibt
true
nur zurück, wenn beide Operanden dieselben Zeichen in derselben Reihenfolge haben. - Zahl: gibt
true
nur zurück, wenn beide Operanden denselben Wert haben.+0
und-0
werden als derselbe Wert behandelt. Wenn einer der OperandenNaN
ist, wirdfalse
zurückgegeben; daher istNaN
nie gleichNaN
. - Boolean: gibt
true
nur zurück, wenn beide Operandentrue
oder beidefalse
sind. - BigInt: gibt
true
nur zurück, wenn beide Operanden denselben Wert haben. - Symbol: gibt
true
nur zurück, wenn beide Operanden auf dasselbe Symbol verweisen.
- Objekt: gibt
- Wenn einer der Operanden
null
oderundefined
ist, muss der andere ebenfallsnull
oderundefined
sein, umtrue
zurückzugeben. Andernfalls wirdfalse
zurückgegeben. - Wenn einer der Operanden ein Objekt ist und der andere ein Primitiv, wandeln Sie das Objekt in ein Primitiv um.
- An diesem Schritt werden beide Operanden in Primitiva umgewandelt (eines von String, Number, Boolean, Symbol und BigInt). Der Rest der Konvertierung wird fallweise durchgeführt.
- Wenn sie denselben Typ haben, vergleichen Sie sie mit Schritt 1.
- Wenn einer der Operanden ein Symbol ist, der andere jedoch nicht, geben Sie
false
zurück. - Wenn einer der Operanden ein Boolean ist, der andere jedoch nicht, wandeln Sie den Boolean in eine Zahl um:
true
wird in 1 konvertiert undfalse
wird in 0 konvertiert. Vergleichen Sie dann die beiden Operanden erneut lose. - Zahl zu String: wandeln Sie den String in eine Zahl um. Ein Konvertierungsfehler führt zu
NaN
, was garantiert, dass die Gleichheitfalse
ist. - Zahl zu BigInt: Vergleichen Sie sie nach ihrem mathematischen Wert. Wenn die Zahl ±Unendlichkeit oder
NaN
ist, geben Siefalse
zurück. - String zu BigInt: Wandeln Sie den String unter Verwendung desselben Algorithmus wie der
BigInt()
Konstruktor in ein BigInt um. Wenn die Konvertierung fehlschlägt, geben Siefalse
zurück.
Traditionell und gemäß ECMAScript sind alle Primitiva und Objekte lose ungleich undefined
und null
. Aber die meisten Browser erlauben einer sehr kleinen Klasse von Objekten (insbesondere dem document.all
-Objekt für jede Seite), in einigen Kontexten so zu handeln, als ob sie den Wert undefined
emulieren. Lose Gleichheit ist ein solcher Kontext: null == A
und undefined == A
werten sich zu true aus, wenn und nur wenn A ein Objekt ist, das undefined
emuliert. In allen anderen Fällen ist ein Objekt niemals lose gleich undefined
oder null
.
In den meisten Fällen wird von der Verwendung loser Gleichheit abgeraten. Das Ergebnis eines Vergleichs mit strikter Gleichheit ist leichter vorherzusagen und kann aufgrund des Fehlens von Typumwandlungen auch schneller ausgewertet werden.
Das folgende Beispiel demonstriert lose Gleichheitsvergleiche, die die Zahl 0
, das BigInt 0n
, den String '0'
und ein Objekt betreffen, dessen toString()
-Wert '0'
ist.
const num = 0;
const big = 0n;
const str = "0";
const obj = new String("0");
console.log(num == str); // true
console.log(big == num); // true
console.log(str == big); // true
console.log(num == obj); // true
console.log(big == obj); // true
console.log(str == obj); // true
Lose Gleichheit wird nur vom ==
Operator verwendet.
Gleichheit gleicher Werte mit Object.is()
Die Gleichheit gleicher Werte bestimmt, ob zwei Werte funktional identisch in allen Kontexten sind. (Dieser Anwendungsfall zeigt ein Beispiel für das Liskov-Substitutionsprinzip.) Ein Beispiel tritt auf, wenn versucht wird, eine unveränderliche Eigenschaft zu ä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 Ausnahme auslösen, wenn versucht wird, eine unveränderliche Eigenschaft zu ändern, es wird jedoch nichts getan, wenn keine tatsächliche Änderung angefordert wird. Wenn v
-0
ist, wurde keine Änderung angefordert und es wird kein Fehler ausgelöst. Intern wird beim erneuten Definieren einer unveränderlichen Eigenschaft der neu angegebene Wert mit dem aktuellen Wert mittels der Gleichheit gleicher Werte verglichen.
Die Gleichheit gleicher Werte wird durch die Methode Object.is
bereitgestellt. Sie wird fast überall in der Sprache verwendet, wo ein Wert mit äquivalenter Identität erwartet wird.
Same-value-zero Gleichheit
Ähnlich wie die Gleichheit gleicher Werte, werden bei der Same-value-zero Gleichheit jedoch +0 und -0 als gleich betrachtet.
Same-value-zero Gleichheit ist nicht als JavaScript-API verfügbar, kann jedoch mit benutzerdefiniertem Code implementiert werden:
function sameValueZero(x, y) {
if (typeof x === "number" && typeof y === "number") {
// x and y are equal (may be -0 and 0) or they are both NaN
return x === y || (x !== x && y !== y);
}
return x === y;
}
Same-value-zero unterscheidet sich von strikter Gleichheit dadurch, dass NaN
äquivalent behandelt wird, und von der Gleichheit gleicher Werte dadurch, dass -0
als äquivalent zu 0
behandelt wird. In der Regel hat sie daher das vernünftigste Verhalten bei der Suche, insbesondere beim Arbeiten mit NaN
. Sie wird von Array.prototype.includes()
, TypedArray.prototype.includes()
sowie von Methoden der Klassen Map
und Set
verwendet, um die Schlüsselgleichheit zu vergleichen.
Vergleich von Gleichheitsmethoden
Menschen vergleichen oft doppelt gleich und dreifach gleich, indem sie sagen, dass eines eine "verbesserte" Version des anderen ist. Zum Beispiel könnte man doppelt gleich als eine erweiterte Version von dreifach gleich bezeichnen, da ersteres alles tut, was letzteres tut, jedoch mit Typkonvertierung der Operanden – zum Beispiel 6 == "6"
. Alternativ könnte man argumentieren, dass doppelt gleich die Basislinie ist und dreifach gleich eine verbesserte Version, da es erfordert, dass die beiden Operanden denselben Typ haben, also fügt es eine zusätzliche Einschränkung hinzu.
Diese Denkweise impliziert jedoch, dass die Gleichheitsvergleiche ein eindimensionales "Spektrum" bilden, bei dem "vollständig strikt" an einem Ende und "vollständig lose" am anderen Ende liegt. Dieses Modell versagt bei Object.is
, da es weder "lockerer" als doppelt gleich ist, noch "strenger" als dreifach gleich, noch irgendwo dazwischen passt (d.h. sowohl strenger als doppelt gleich, aber lockerer als dreifach gleich). Wir können aus der folgende Tabelle für Gleichheitsvergleiche sehen, dass dies auf die Art und Weise zurückzuführen ist, wie Object.is
mit NaN
umgeht. Beachten Sie, dass wir sagen könnten, es passe in das Spektrum von locker zu strikt als eine noch striktere Form von dreifach gleich, die zwischen -0
und +0
unterscheidet, wenn Object.is(NaN, NaN)
zu false
ausgewertet würde. Die NaN
-Behandlung bedeutet allerdings, dass dies nicht der Fall ist. Leider muss Object.is
in Bezug auf seine spezifischen Merkmale statt durch seine Lockerheit oder Strenge im Vergleich zu den Gleichheitsoperatoren betrachtet werden.
x | y | == |
=== |
Object.is |
SameValueZero |
---|---|---|---|---|---|
undefined |
undefined |
✅ true |
✅ true |
✅ true |
✅ true |
null |
null |
✅ true |
✅ true |
✅ true |
✅ true |
true |
true |
✅ true |
✅ true |
✅ true |
✅ true |
false |
false |
✅ true |
✅ true |
✅ true |
✅ true |
'foo' |
'foo' |
✅ true |
✅ true |
✅ true |
✅ true |
0 |
0 |
✅ true |
✅ true |
✅ true |
✅ true |
+0 |
-0 |
✅ true |
✅ true |
❌ false |
✅ true |
+0 |
0 |
✅ true |
✅ true |
✅ true |
✅ true |
-0 |
0 |
✅ true |
✅ true |
❌ false |
✅ true |
0n |
-0n |
✅ true |
✅ true |
✅ true |
✅ true |
0 |
false |
✅ true |
❌ false |
❌ false |
❌ false |
"" |
false |
✅ true |
❌ false |
❌ false |
❌ false |
"" |
0 |
✅ true |
❌ false |
❌ false |
❌ false |
'0' |
0 |
✅ true |
❌ false |
❌ false |
❌ false |
'17' |
17 |
✅ true |
❌ false |
❌ false |
❌ false |
[1, 2] |
'1,2' |
✅ true |
❌ false |
❌ false |
❌ false |
new String('foo') |
'foo' |
✅ true |
❌ false |
❌ false |
❌ false |
null |
undefined |
✅ true |
❌ false |
❌ false |
❌ false |
null |
false |
❌ false |
❌ false |
❌ false |
❌ false |
undefined |
false |
❌ false |
❌ false |
❌ false |
❌ false |
{ foo: 'bar' } |
{ foo: 'bar' } |
❌ false |
❌ false |
❌ false |
❌ false |
new String('foo') |
new String('foo') |
❌ false |
❌ false |
❌ false |
❌ false |
0 |
null |
❌ false |
❌ false |
❌ false |
❌ false |
0 |
NaN |
❌ false |
❌ false |
❌ false |
❌ false |
'foo' |
NaN |
❌ false |
❌ false |
❌ false |
❌ false |
NaN |
NaN |
❌ false |
❌ false |
✅ true |
✅ true |
Wann man Object.is() anstelle von dreifach gleich verwendet
Im Allgemeinen ist das einzige Mal, dass das spezielle Verhalten von Object.is
gegenüber Nullen wahrscheinlich von Interesse ist, im Streben nach bestimmten Metaprogrammierungsschemata, insbesondere im Hinblick auf Eigenschaftsdeskriptoren, wenn es wünschenswert ist, dass Ihre Arbeit einige der Merkmale von Object.defineProperty
widerspiegelt. Wenn Ihr Anwendungsfall dies nicht erfordert, wird empfohlen, Object.is
zu vermeiden und stattdessen ===
zu verwenden. Selbst wenn Ihre Anforderungen umfassen, dass Vergleiche zwischen zwei NaN
Werten zu true
ausgewertet werden, ist es im Allgemeinen einfacher, die NaN
-Überprüfungen (unter Verwendung der isNaN
-Methode aus früheren ECMAScript-Versionen) als Sonderfall zu behandeln, als herauszufinden, wie umgebende Berechnungen das Vorzeichen von Nullen, die Ihnen bei Ihrem Vergleich begegnen, beeinflussen könnten.
Hier ist eine nicht erschöpfende Liste von eingebauten Methoden und Operatoren, die in Ihrem Code eine Unterscheidung zwischen -0
und +0
hervorrufen könnten:
-
(unäres Negieren)-
Betrachten Sie das folgende Beispiel:
jsconst stoppingForce = obj.mass * -obj.velocity;
Wenn
obj.velocity
0
ist (oder zu0
berechnet wird), wird an dieser Stelle ein-0
eingeführt und breitet sich instoppingForce
aus. Math.atan2
,Math.ceil
,Math.pow
,Math.round
-
In einigen Fällen ist es möglich, dass durch diese Methoden ein
-0
in einen Ausdruck eingeführt wird, obwohl kein-0
als einer der Parameter existiert. Zum Beispiel ergibtMath.pow
, dass-Infinity
mit einer negativen, ungeraden Potenz potenziert wird,-0
. Weitere Informationen finden Sie in der Dokumentation der einzelnen Methoden. Math.floor
,Math.max
,Math.min
,Math.sin
,Math.sqrt
,Math.tan
-
Es ist möglich, dass diese Methoden in einigen Fällen einen
-0
zurückgeben, wenn ein-0
als einer der Parameter existiert. Zum Beispiel ergibtMath.min(-0, +0)
-0
. Weitere Informationen finden Sie in der Dokumentation der einzelnen Methoden. ~
,<<
,>>
-
Jeder dieser Operatoren verwendet den ToInt32-Algorithmus intern. Da es nur eine Darstellung für 0 im internen 32-Bit-Ganzzahldatentyp gibt, wird
-0
einen Rundtrip nach einer inversen Operation nicht überleben. Zum Beispiel evaluieren sowohlObject.is(~~(-0), -0)
als auchObject.is(-0 << 2 >> 2, -0)
zufalse
.
Verlassen Sie sich bei Object.is
nicht darauf, wenn das Vorzeichen von Nullen nicht berücksichtigt wird. Natürlich tut es genau das gewollte, wenn die Absicht besteht, zwischen -0
und +0
zu unterscheiden.
Warnung: Object.is() und NaN
Die Object.is
-Spezifikation behandelt alle Instanzen von NaN
als dasselbe Objekt. Da jedoch typisierte Arrays verfügbar sind, können wir unterschiedliche Fließkomma-Darstellungen von NaN
haben, die sich nicht in allen Kontexten identisch verhalten. Zum Beispiel:
const f2b = (x) => new Uint8Array(new Float64Array([x]).buffer);
const b2f = (x) => new Float64Array(x.buffer)[0];
// Get a byte representation of NaN
const n = f2b(NaN);
// Change the first bit, which is the sign bit and doesn't matter for NaN
n[0] = 1;
const nan2 = b2f(n);
console.log(nan2); // NaN
console.log(Object.is(nan2, NaN)); // true
console.log(f2b(NaN)); // Uint8Array(8) [0, 0, 0, 0, 0, 0, 248, 127]
console.log(f2b(nan2)); // Uint8Array(8) [1, 0, 0, 0, 0, 0, 248, 127]