Gleichheitsvergleiche und Ähnlichkeit
JavaScript bietet drei verschiedene Wertvergleichsoperationen:
===
— strikte Gleichheit (dreifach gleich)==
— lose Gleichheit (doppelt gleich)Object.is()
Welche Operation 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, um IEEE 754 zu entsprechen (alsoNaN != NaN
und-0 == +0
); - Dreifach gleich (
===
) führt denselben Vergleich durch wie doppelt gleich (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 (es verhält sich also wie===
, außer bei diesen 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 die Unterscheidung dieser alle mit ihrer Behandlung von Primitiven beschäftigt; keiner von ihnen vergleicht, ob die Parameter konzeptionell ähnlich in der Struktur sind. Für alle nicht-primitiven Objekte x
und y
, die dieselbe Struktur haben, aber unterschiedliche Objekte an sich sind, wird jede der oben genannten Formen zu false
ausgewertet.
Strikte Gleichheit mit ===
Strikte Gleichheit vergleicht zwei Werte auf Gleichheit. Keiner der Werte wird implizit in einen anderen Wert konvertiert, bevor er verglichen wird. Wenn die Werte unterschiedliche Typen haben, werden die Werte als ungleich betrachtet. Wenn die Werte denselben Typ haben, keine Zahlen sind und denselben Wert haben, werden sie als gleich betrachtet. Schließlich, wenn beide Werte Zahlen sind, werden sie als gleich angesehen, 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. Für alle Werte außer Zahlen verwendet sie die offensichtlichen Semantiken: Ein Wert ist nur gleich sich selbst. Für Zahlen verwendet sie leicht unterschiedliche Semantiken, um zwei verschiedene Sonderfälle zu übergehen. Der erste ist, dass Gleitpunkt-Null entweder positiv oder negativ signiert ist. Dies ist nützlich bei der Darstellung bestimmter mathematischer Lösungen, aber da die meisten Situationen keinen Unterschied zwischen +0
und -0
machen, behandelt strikte Gleichheit sie als denselben Wert. Der zweite ist, dass Gleitpunkt das Konzept eines Nicht-Eine-Zahl-Wertes, NaN
, enthält, um die Lösung für bestimmte undefinierte mathematische 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 Array-Index-Suchmethoden wie Array.prototype.indexOf()
, Array.prototype.lastIndexOf()
, TypedArray.prototype.indexOf()
, TypedArray.prototype.lastIndexOf()
und case
-Abgleich verwendet. Das bedeutet, dass Sie indexOf(NaN)
nicht verwenden können, um den Index eines NaN
-Wertes in einem Array zu finden, oder NaN
als case
-Wert in einer switch
-Anweisung verwenden und es mit etwas 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 alle Werte von A
und B
(außer in der 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
true
nur zurück, wenn beide Operanden dasselbe Objekt referenzieren. - 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, geben Siefalse
zurück; daher istNaN
niemals 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 dasselbe Symbol referenzieren.
- Objekt: gibt
- Wenn einer der Operanden
null
oderundefined
ist, muss der andere ebenfallsnull
oderundefined
sein, umtrue
zurückzugeben. Andernfalls geben Siefalse
zurück. - Wenn einer der Operanden ein Objekt ist und der andere ein primitiver Wert ist, konvertieren Sie das Objekt in ein primitives.
- An diesem Punkt werden beide Operanden in Primitive konvertiert (einer von String, Number, Boolean, Symbol und BigInt). Der Rest der Konvertierung erfolgt fallweise.
- Wenn sie vom selben Typ sind, 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, konvertieren Sie den Boolean in eine Zahl:
true
wird in 1 undfalse
in 0 konvertiert. Vergleichen Sie dann die beiden Operanden erneut locker. - Zahl zu String: konvertieren Sie den String in eine Zahl. Bei Konvertierungsfehlern ergibt sich
NaN
, was die Gleichheit garantiertfalse
ist. - Zahl zu BigInt: vergleichen Sie nach ihrem mathematischen Wert. Wenn die Zahl ±Infinity oder
NaN
ist, geben Siefalse
zurück. - String zu BigInt: konvertieren Sie den String in einen BigInt mit demselben Algorithmus wie der
BigInt()
-Konstruktor. Wenn die Konvertierung fehlschlägt, geben Siefalse
zurück.
Traditionell und gemäß ECMAScript sind alle Primitivwerte und Objekte lose ungleich zu undefined
und null
. Aber die meisten Browser erlauben einer sehr engen Klasse von Objekten (insbesondere dem document.all
-Objekt für jede Seite), in einigen Kontexten, sich so zu verhalten, als ob sie den Wert undefined
emulieren. Lose Gleichheit ist ein solcher Kontext: null == A
und undefined == A
werten zu true
aus, wenn, und nur wenn, A ein Objekt ist, das undefined
emuliert. In allen anderen Fällen ist ein Objekt niemals los gleich undefined
oder null
.
In den meisten Fällen wird die Verwendung von loser Gleichheit nicht empfohlen. Das Ergebnis eines Vergleichs mit strikter Gleichheit ist leichter vorherzusagen und kann aufgrund des Fehlens von Typumwandlungen schneller ausgewertet werden.
Das folgende Beispiel zeigt Vergleiche mit loser Gleichheit, die den Zahlen-Primitiv 0
, den BigInt-Primitiv 0n
, den Zeichenfolgen-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 vom ==
-Operator verwendet.
Gleichheit mit demselben Wert durch Object.is()
Gleichheit mit demselben Wert bestimmt, ob zwei Werte in allen Kontexten funktional identisch sind. (Dieser Anwendungsfall demonstriert ein Beispiel des Liskov-Substitutionsprinzips.) Ein Fall 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, tut jedoch nichts, wenn keine tatsächliche Änderung angefragt wird. Wenn v
-0
ist, wurde keine Änderung angefragt und kein Fehler wird geworfen. Intern wird beim Neudefinieren einer unveränderlichen Eigenschaft der neu spezifizierte Wert mit dem aktuellen Wert durch Gleichheit mit demselben Wert verglichen.
Die Gleichheit mit demselben Wert wird von der Object.is
-Methode bereitgestellt. Sie wird fast überall in der Sprache verwendet, wo ein Wert von äquivalenter Identität erwartet wird.
Gleichheit mit demselben Wert Null
Ähnlich wie die Gleichheit mit demselben Wert, jedoch werden +0 und -0 als gleich betrachtet.
Die Gleichheit mit demselben Wert Null ist nicht als JavaScript-API zugänglich, 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;
}
Die Gleichheit mit demselben Wert Null unterscheidet sich nur von strikter Gleichheit, indem NaN
als gleich behandelt wird, und nur von Gleichheit mit demselben Wert, indem -0
als gleich zu 0
betrachtet wird. Dies macht sie normalerweise zum sinnvollsten Verhalten bei der Suche, insbesondere beim Arbeiten mit NaN
. Sie wird von Array.prototype.includes()
, TypedArray.prototype.includes()
sowie den Methoden Map
und Set
für den Vergleich der Schlüsselgleichheit verwendet.
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 doppelt gleich als eine erweiterte Version von dreifach gleich beschrieben werden, da erstere alles tut, was letztere macht, jedoch mit Typkonvertierung ihrer Operanden — zum Beispiel 6 == "6"
. Alternativ kann behauptet werden, dass doppelt gleich die Basis ist und dreifach gleich eine verbesserte Version, da es erfordert, dass die beiden Operanden denselben Typ haben, wodurch eine zusätzliche Einschränkung hinzugefügt wird.
Diese Denkweise impliziert jedoch, dass die Gleichheitsvergleiche ein eindimensionales "Spektrum" bilden, bei dem "völlig strikt" am einen Ende und "völlig lose" am anderen Ende liegt. Dieses Modell versagt bei Object.is
, da es nicht "loser" als doppelt gleich oder "strenger" als dreifach gleich ist, noch irgendwo dazwischen passt (d.h. sowohl strenger als doppelt gleich, aber loser als dreifach gleich zu sein). Wir können aus der Ähnlichkeitsvergleichtabelle unten erkennen, dass dies auf die Art zurückzuführen ist, wie Object.is
mit NaN
umgeht. Beachten Sie, dass, wenn Object.is(NaN, NaN)
zu false
ausgewertet würde, wir könnten sagen, dass es auf dem lose/strengen Spektrum als eine noch strengere Form von dreifach gleich passt, eine, die zwischen -0
und +0
unterscheidet. Das NaN
-Handling bedeutet jedoch, dass dies unzutreffend ist. Leider muss Object.is
in Bezug auf seine spezifischen Eigenschaften betrachtet werden, anstatt auf seinen Grad an Lockerheit oder Striktheit in Bezug 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 verwendet man Object.is() anstelle von dreifach gleich
Im Allgemeinen ist das einzige Mal, wenn Object.is
s spezielles Verhalten gegenüber Nullen von Interesse sein könnte, bei der Verfolgung bestimmter Metaprogrammierungsschemata, insbesondere in Bezug auf Eigenschaftenbeschreibungen, wenn es wünschenswert ist, dass Ihre Arbeit einige der Eigenschaften von Object.defineProperty
widerspiegelt. Wenn Ihr Anwendungsfall dies nicht erfordert, wird empfohlen, 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
-Überprüfungen (mit der isNaN
-Methode aus vorherigen Versionen von ECMAScript) speziell zu behandeln, als herauszufinden, wie umgebende Berechnungen das Vorzeichen von Nullwerten beeinflussen könnten, denen Sie in Ihrem Vergleich begegnen.
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 zutage tritt:
-
(unäres Minus)-
Betrachten Sie das folgende Beispiel:
jsconst stoppingForce = obj.mass * -obj.velocity;
Wenn
obj.velocity
0
ist (oder auf0
berechnet wird), wird in dieser Position ein-0
eingeführt und breitet sich nach draußen instoppingForce
aus. Math.atan2
,Math.ceil
,Math.pow
,Math.round
-
In einigen Fällen ist es möglich, dass ein
-0
in einen Ausdruck als Rückgabewert dieser Methoden eingeführt wird, auch wenn kein-0
als einer der Parameter existiert. Zum Beispiel:Math.pow
, um-Infinity
zur Potenz eines negativen, ungeraden Exponenten zu erheben, wird zu-0
ausgewertet. Siehe die Dokumentation zu den einzelnen Methoden. Math.floor
,Math.max
,Math.min
,Math.sin
,Math.sqrt
,Math.tan
-
Es ist möglich, einen
-0
-Rückgabewert aus diesen Methoden in einigen Fällen zu erhalten, in denen ein-0
als einer der Parameter existiert. Z.B.Math.min(-0, +0)
wird zu-0
ausgewertet. Siehe die Dokumentation zu den einzelnen Methoden. ~
,<<
,>>
-
Jeder dieser Operatoren verwendet den ToInt32-Algorithmus intern. Da es nur eine Darstellung für 0 in der internen 32-Bit-Ganzzahltyp gibt, wird
-0
nach einer inversen Operation keine Rundreise überleben. Z.B. sowohlObject.is(~~(-0), -0)
als auchObject.is(-0 << 2 >> 2, -0)
werden zufalse
ausgewertet.
Sich auf Object.is
zu verlassen, wenn das Vorzeichen von Null nicht berücksichtigt wird, kann gefährlich sein. Natürlich, wenn die Absicht darin besteht, zwischen -0
und +0
zu unterscheiden, tut es genau, was gewünscht ist.
Hinweis: 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. 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]