Gleichheitsvergleiche und Identität
JavaScript bietet drei verschiedene Wert-Vergleichsoperationen:
===— strikte Gleichheit (drei Gleichheitszeichen)==— lose Gleichheit (zwei Gleichheitszeichen)Object.is()
Welche Operation Sie wählen, hängt davon ab, welche Art von Vergleich Sie durchführen möchten. Kurz gesagt:
- Doppelte Gleichheit (
==) führt eine Typkonvertierung durch, wenn zwei Dinge verglichen werden, und behandeltNaN,-0und+0speziell, um dem IEEE 754-Standard zu entsprechen (alsoNaN != NaNund-0 == +0); - Dreifache Gleichheit (
===) führt den gleichen Vergleich wie die doppelte Gleichheit durch (einschließlich der speziellen Behandlung fürNaN,-0und+0), jedoch ohne Typkonvertierung; wenn die Typen unterschiedlich sind, wirdfalsezurückgegeben. Object.is()führt keine Typkonvertierung durch und behandeltNaN,-0und+0nicht speziell (dabei verhält es sich wie===, außer bei den speziellen numerischen Werten).
Sie entsprechen drei von vier Gleichheitsalgorithmen in JavaScript:
- IsLooselyEqual:
== - IsStrictlyEqual:
=== - SameValue:
Object.is() - SameValueZero: wird von vielen eingebauten Operationen verwendet
Beachten Sie, dass sich der Unterschied zwischen ihnen auf die Behandlung von einfachen Werten bezieht; keiner von ihnen vergleicht, ob die Parameter konzeptionell ähnlich in ihrer Struktur sind. Für nicht-primitive Objekte x und y, die die gleiche Struktur, aber unterschiedliche Objekte sind, werden alle oben genannten Formen zu false ausgewertet.
Der Vergleich der Inhalte von separaten Objekten oder Arrays wird als tiefe Gleichheit bezeichnet. JavaScript stellt keinen allgemeinen Operator für tiefe Vergleiche bereit; Bibliotheken und Host-APIs können Vergleichs-Utilities mit unterschiedlichen Regeln bereitstellen.
Strikte Gleichheit mit ===
Strikte Gleichheit vergleicht zwei Werte auf Gleichheit. Kein Wert wird implizit in einen anderen Wert konvertiert, bevor er verglichen wird. Wenn die Werte unterschiedliche Typen haben, werden sie als ungleich betrachtet. Wenn die Werte denselben Typ haben, keine Zahlen sind und denselben Wert haben, werden sie als gleich betrachtet. Schließlich werden, wenn beide Werte Zahlen sind, sie als gleich betrachtet, wenn sie beide nicht NaN sind und denselben Wert haben, oder wenn einer +0 und der andere -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 es die offensichtliche Semantik: Ein Wert ist nur gleich sich selbst. Für Zahlen werden leicht unterschiedliche Semantiken verwendet, um zwei verschiedene Randfälle zu glätten. Der erste ist, dass das Gleitkommadarstellung von Null entweder positiv oder negativ ist. Dies ist nützlich, um bestimmte mathematische Lösungen darzustellen, aber da in den meisten Situationen der Unterschied zwischen +0 und -0 nicht wichtig ist, behandelt strikte Gleichheit sie als denselben Wert. Der zweite ist, dass das Gleitkomma den Begriff eines Nicht-Zahl-Werts, NaN, enthält, um die Lösung für bestimmte ill-defined mathematische Probleme darzustellen: zum Beispiel negative Unendlichkeit, die zu positiver Unendlichkeit addiert wird. Strikte Gleichheit behandelt NaN als ungleich zu jedem anderen Wert — einschließlich sich selbst. (Der einzige Fall, in dem (x !== x) wahr ist, ist, wenn x NaN ist.)
Neben === wird auch von Array-Indexfindungsmethoden wie Array.prototype.indexOf(), Array.prototype.lastIndexOf(), TypedArray.prototype.indexOf(), TypedArray.prototype.lastIndexOf() und case-Matching verwendet. Das bedeutet, dass Sie indexOf(NaN) nicht verwenden können, um den Index eines NaN-Werts in einem Array zu finden, oder NaN als case-Wert in einer switch-Anweisung verwenden und es mit irgendetwas abgleichen können.
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 identische Semantik zu B == A für beliebige Werte von A und B (außer für die Reihenfolge der angewendeten Konvertierungen). Das Verhalten bei der Durchführung von loser Gleichheit mit == ist wie folgt:
- Wenn die Operanden denselben Typ haben, werden sie wie folgt verglichen:
- Objekt: gibt
truenur zurück, wenn beide Operanden auf dasselbe Objekt verweisen. - String: gibt
truenur zurück, wenn beide Operanden dieselben Zeichen in derselben Reihenfolge haben. - Zahl: gibt
truenur zurück, wenn beide Operanden denselben Wert haben.+0und-0werden als derselbe Wert behandelt. Wenn einer der OperandenNaNist, gibt erfalsezurück; also istNaNniemals gleichNaN. - Boolean: gibt
truenur zurück, wenn beide Operanden sowohltrueals auchfalsesind. - BigInt: gibt
truenur zurück, wenn beide Operanden denselben Wert haben. - Symbol: gibt
truenur zurück, wenn beide Operanden auf dasselbe Symbol verweisen.
- Objekt: gibt
- Wenn einer der Operanden
nulloderundefinedist, muss der andere auchnulloderundefinedsein, umtruezurückzugeben. Andernfalls gibt erfalsezurück. - Wenn einer der Operanden ein Objekt und der andere ein primitiver Wert ist, konvertieren Sie das Objekt in einen primitiven Wert.
- An diesem Schritt werden beide Operanden in primitive Werte konvertiert (einer von String, Number, Boolean, Symbol und BigInt). Der Rest der Konvertierung erfolgt fallweise.
- Wenn sie denselben Typ haben, vergleichen Sie sie mit Schritt 1.
- Wenn einer der Operanden ein Symbol ist, der andere jedoch nicht, geben Sie
falsezurück. - Wenn einer der Operanden ein Boolean ist, der andere jedoch nicht, konvertieren Sie das Boolean in eine Zahl:
truewird zu 1 undfalsewird zu 0 konvertiert. Vergleichen Sie dann die beiden Operanden erneut lose. - Zahl zu String: Konvertieren Sie den String in eine Zahl. Ein Konvertierungsfehler führt zu
NaN, was garantiert, dass die Gleichheitfalseist. - Zahl zu BigInt: Vergleichen Sie anhand ihres mathematischen Wertes. Wenn die Zahl ±Unendlichkeit oder
NaNist, geben Siefalsezurück. - String zu BigInt: Konvertieren Sie den String in einen BigInt, indem Sie denselben Algorithmus wie im
BigInt()-Konstruktor verwenden. Wenn die Konvertierung fehlschlägt, geben Siefalsezurück.
Traditionell und gemäß ECMAScript sind alle primitiven Werte und Objekte lose ungleich undefined und null. Aber die meisten Browser erlauben eine sehr kleine Klasse von Objekten (insbesondere das 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 zu wahr 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 der losen Gleichheit abgeraten. Das Ergebnis eines Vergleichs mit strikter Gleichheit ist leichter vorherzusagen und kann schneller ausgewertet werden, da keine Typumwandlung erfolgt.
Das folgende Beispiel zeigt lose Gleichheitsvergleiche, die das nummerische Primitive 0, das BigInt-Primitiv 0n, das String-Primitiv '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 durch den ==-Operator verwendet.
Gleichwertigkeit mit gleichem Wert unter Verwendung von Object.is()
Die Gleichwertigkeit mit gleichem Wert bestimmt, ob zwei Werte in allen Kontexten funktional identisch 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 wirft eine Ausnahme, wenn versucht wird, eine unveränderliche Eigenschaft zu ändern, aber es passiert nichts, wenn keine tatsächliche Änderung angefordert wird. Wenn v -0 ist, wurde keine Änderung angefordert, und es wird kein Fehler ausgelöst. Intern, wenn eine unveränderliche Eigenschaft neu definiert wird, wird der neu spezifizierte Wert mit dem aktuellen Wert unter Verwendung der Gleichwertigkeit mit gleichem Wert verglichen.
Die Gleichwertigkeit mit gleichem Wert wird durch die Methode Object.is bereitgestellt. Sie wird fast überall in der Sprache verwendet, wo ein Wert mit äquivalenter Identität erwartet wird.
Gleichwertigkeit mit gleichem Wert Null
Ähnlich wie die Gleichwertigkeit mit gleichem Wert, jedoch werden +0 und -0 als gleich betrachtet.
Die Gleichwertigkeit mit gleichem Wert Null wird nicht als JavaScript-API bereitgestellt, kann aber 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;
}
Gleichwertigkeit mit gleichem Wert Null unterscheidet sich nur von strikter Gleichheit, indem sie NaN als gleichwertig behandelt, und unterscheidet sich nur von Gleichwertigkeit mit gleichem Wert, indem sie -0 als gleichwertig zu 0 behandelt. Dies führt dazu, dass sie meistens das sinnvollste Verhalten während des Suchens hat, insbesondere wenn mit NaN gearbeitet wird. Sie wird von Array.prototype.includes(), TypedArray.prototype.includes() sowie von Map und Set-Methoden verwendet, um die Gleichheit von Schlüsseln zu vergleichen.
Vergleich von Gleichheitsmethoden
Menschen vergleichen oft doppelte Gleichheit und dreifache Gleichheit, indem sie sagen, eine sei eine "verbesserte" Version der anderen. Zum Beispiel könnte man sagen, dass doppelte Gleichheit eine erweiterte Version der dreifachen Gleichheit ist, weil die erstere alles tut, was die letztere tut, jedoch mit Typkonvertierung auf ihren Operanden — zum Beispiel 6 == "6". Alternativ kann behauptet werden, dass doppelte Gleichheit die Basislinie ist und die dreifache Gleichheit eine verbesserte Version, da sie von den beiden Operanden erfordert, denselben Typ zu haben, wodurch eine zusätzliche Einschränkung hinzugefügt wird.
Diese Denkweise impliziert jedoch, dass die Gleichheitsvergleiche ein eindimensionales "Spektrum" bilden, wobei "völlig strikt" am einen Ende und "völlig lose" am anderen Ende liegt. Dieses Modell scheitert mit Object.is, da es nicht "lockerer" als die doppelte Gleichheit oder "strikter" als die dreifache Gleichheit ist, noch passt es irgendwo dazwischen (d.h. gleichzeitig strikter als die doppelte Gleichheit, aber lockerer als die dreifache Gleichheit). Wir können aus der Tabelle der Gleichheitsvergleiche unten sehen, dass dies auf die Art und Weise zurückzuführen ist, wie Object.is NaN behandelt. Beachten Sie, dass, wenn Object.is(NaN, NaN) zu false bewertet würde, wir sagen könnten, dass es auf dem locker/strikt-Spektrum als eine noch striktere Form der dreifachen Gleichheit passt, eine, die zwischen -0 und +0 unterscheidet. Die Handhabung von NaN bedeutet jedoch, dass dies nicht zutrifft. Leider muss Object.is in Bezug auf seine spezifischen Eigenschaften betrachtet werden, anstatt in Bezug auf seine Lockerheit oder Striktheit im Hinblick auf die Gleichheitsoperatoren.
| 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 sollte Object.is() gegenüber der dreifachen Gleichheit verwendet werden?
Im Allgemeinen ist die einzige Zeit, in der das spezielle Verhalten von Object.is gegenüber Nullen von Interesse sein könnte, bei der Verfolgung bestimmter Metaprogrammierungsschemata, insbesondere bezüglich Eigenschaftsdeskriptoren, wenn es wünschenswert ist, dass Ihre Arbeit einige der Eigenschaften von Object.defineProperty widerspiegelt. Wenn Ihr Anwendungsfall dies nicht erfordert, wird vorgeschlagen, Object.is zu vermeiden und stattdessen === zu verwenden. Selbst wenn Ihre Anforderungen beinhalten, dass Vergleiche zwischen zwei NaN-Werten zu true ausgewertet werden, ist es im Allgemeinen einfacher, die NaN-Prüfungen (mit der isNaN-Methode, die in früheren ECMAScript-Versionen verfügbar ist) als Sonderfall zu behandeln, als herauszufinden, wie umliegende Berechnungen das Vorzeichen der Nullen, die Sie in Ihrem Vergleich finden, beeinflussen könnten.
Hier ist eine nicht erschöpfende Liste von eingebauten Methoden und Operatoren, die dazu führen könnten, dass eine Unterscheidung zwischen -0 und +0 in Ihrem Code manifestiert wird:
-(unäre Negation)-
Betrachten Sie das folgende Beispiel:
jsconst stoppingForce = obj.mass * -obj.velocity;Wenn
obj.velocity0ist (oder zu0berechnet wird), wird an dieser Stelle ein-0eingeführt und verbreitet sich instoppingForce. Math.atan2,Math.ceil,Math.pow,Math.round-
In einigen Fällen ist es möglich, dass ein
-0als Rückgabewert dieser Methoden in einen Ausdruck eingeführt wird, selbst wenn kein-0als einer der Parameter existiert. Zum Beispiel ergibt die Verwendung vonMath.pow, um-Infinityzu einer negativen, ungeraden Exponentiation zu erheben,-0. Verweisen Sie auf die Dokumentation der einzelnen Methoden. Math.floor,Math.max,Math.min,Math.sin,Math.sqrt,Math.tan-
Es ist möglich, einen
-0Rückgabewert aus diesen Methoden in einigen Fällen zu erhalten, bei denen ein-0als einer der Parameter existiert. Zum Beispiel ergibtMath.min(-0, +0)-0. Verweisen Sie auf die Dokumentation der einzelnen Methoden. ~,<<,>>-
Jeder dieser Operatoren verwendet intern den ToInt32-Algorithmus. Da es nur eine Darstellung für 0 im internen 32-Bit-Integer-Typ gibt, überlebt
-0eine Rundreise nach einer inversen Operation nicht. Zum Beispiel ergeben sowohlObject.is(~~(-0), -0)als auchObject.is(-0 << 2 >> 2, -0)false.
Die Verwendung von Object.is kann gefährlich sein, wenn das Vorzeichen der Nullen nicht berücksichtigt wird. Natürlich macht es genau das, was beabsichtigt ist, wenn die Absicht besteht, zwischen -0 und +0 zu unterscheiden.
Achtung: 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 Gleitpunktdarstellungen von NaN haben, die sich nicht in allen Kontexten identisch verhalten. Beispielsweise:
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[7] |= 0x80;
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) [0, 0, 0, 0, 0, 0, 248, 255]
Hinweis:
Implementierungen dürfen die Bit-Darstellung von NaN kanonisieren, sodass nan2, wenn es zurück in Gleitpunkt konvertiert wird, dieselbe Bit-Darstellung wie das ursprüngliche NaN haben kann.