Scriptease.js: Vergleiche mit typsicheren Operatoren

Zwei Variablen zu vergleichen ist eine der grundlegenden Aufgaben jeder Programmiersprache. Auf den ersten Blick scheint die Aufgabe trivial: Wenn beide Variablen den gleichen Wert enthalten, sind sie gleich, ansonsten ungleich. Wer jedoch einen zweiten Blick riskiert, entdeckt einige Stolperfallen.

In Pocket speichern vorlesen Druckansicht 39 Kommentare lesen
Lesezeit: 9 Min.
Von
  • Golo Roden
Inhaltsverzeichnis

Zwei Variablen zu vergleichen ist eine der grundlegenden Aufgaben jeder Programmiersprache. Erst einmal scheint die Aufgabe trivial: Wenn beide Variablen den gleichen Wert enthalten, sind sie gleich, ansonsten ungleich. Auf den zweiten Blick entdeckt man jedoch einige Stolperfallen.

In statisch typisierten Sprachen wie C, C++, Java und C# fällt die Antwort leicht: Zwei Wertetypen sind dann gleich, wenn sie den gleichen Wert enthalten; zwei Referenztypen sind es, wenn beide Referenzen auf dasselbe Objekt verweisen. In beiden Fällen gilt, dass der Typ identisch sein muss, typungleiche Variablen lassen sich ohne explizite Konvertierung in der Regel nicht miteinander vergleichen.

Mehr Infos

Was steckt hinter Scriptease.js?

JavaScript erlernt sich deutlich leichter als die meisten anderen Programmiersprachen. Dennoch ist die Sprache nicht per se einfach, ganz im Gegenteil. Die Palette reicht dabei von exotischen und selten verwendeten Schlüsselwörtern bis hin zu unerklärlichem und scheinbar widersprüchlichem Verhalten. Die neue Kolumne Scriptease.js führt in die Feinheiten von JavaScript ein und vermittelt sowohl subtile Eigenarten der Sprache als auch bemerkenswerte Interna.

Wer eine derartige, sich von C ableitende Sprache gewöhnt ist, erwartet vielleicht das gleiche Verhalten auch in JavaScript: Immerhin sieht JavaScript-Code vertraut aus. Er besteht ebenfalls aus geschweiften Klammern und Semikolons. Doch der Eindruck täuscht: Zwar ähnelt die Syntax von JavaScript der von C-Sprachen, die Semantik unterscheidet sich jedoch deutlich.

Hinsichtlich des Typsystems ist einer der wesentlichen Unterschiede die Tatsache, dass JavaScript über ein vollständig dynamisches Typsystem verfügt. Das bedeutet, dass sich der Typ einer Variablen jederzeit ändern kann. Das lässt sich mit dem typeof-Operator auf einfache Weise ermitteln:

var value = 'Hallo Welt';
console.log(typeof value); // => string
value = 23;
console.log(typeof value); // => number

Beim Vergleich zweier Variablen prüft JavaScript ähnlich wie die C-Sprachen prinzipiell auf Wert- beziehungsweise Referenzgleichheit:

var foo = 23,
bar = 23;
console.log(foo == bar); // => true

JavaScript behandelt die Datentypen number und boolean dabei als Wertetypen, Objekte jedoch als Referenztypen. Da Arrays und Funktionen intern ebenfalls als Objekt implementiert sind, werden auch sie bei einem Vergleich auf Referenzgleichheit geprüft – allerdings nicht auf Wertegleichheit:

var foo = [ 23, 42 ],
bar = [ 23, 42 ];
console.log(foo == bar); // => false

Einen Sonderfall stellt der Datentyp string dar, der in JavaScript weder als Werte- noch als Referenztyp angesehen wird: Die variable Länge spricht zwar für einen Referenztypen, jedoch vergleicht der ==-Operator den tatsächlichen Wert. Da Zeichenketten in JavaScript zudem unveränderlich (immutable) sind, lässt sich die Art der Implementierung nicht auf programmatischem Weg ermitteln. Letztlich spielt sie auch keine Rolle: Fakt ist, dass sich Zeichenketten bei Vergleichen wie Wertetypen verhalten:

var foo = 'Hallo Welt!',
bar = 'Hallo Welt!';
console.log(foo == bar); // => true

Für C#-Entwickler sieht das vertraut aus: Dort sind Zeichenketten als Referenztyp implementiert. Die Klasse System.String überschreibt den ==-Operator jedoch derart, dass bei einem Vergleich auf den eigentlichen Wert geprüft wird. Auch Java bietet dieses Verhalten im Rahmen der string.equals-Methode.

Bislang scheint alles vertraut, doch der Teufel steckt im Detail: Der ==-Operator von JavaScript ist nämlich durch das dynamische Typsystem nicht typsicher. Das bedeutet, dass JavaScript bei unterschiedlichen Typen versucht, einen der beiden Typen derart zu konvertieren, dass ein Vergleich möglich wird. Innerhalb der beiden Wertetypen number und boolean gelten daher die Werte 0 und false als gleich:

console.log(0 == false); // => true

Das ermöglicht es in JavaScript unter anderem, numerische Ausdrücke als Bedingungen für Schleifen oder Abfragen zu verwenden, wie das in C und C++ geht. In JavaScript endet der Einfluss der impliziten Konvertierung jedoch nicht bei den Wertetypen. Stattdessen wird bei Bedarf ebenfalls zwischen Werte- und Referenztypen hin und her konvertiert. Das führt dazu, dass man beispielsweise eine Zahl und ihre Repräsentation als Zeichenkette von JavaScript als gleich ansieht:

console.log(23 == '23'); // => true
console.log(0 == '0'); // => true

Das führt über die Transitivität des Vergleichsoperators dazu, dass auch die Zeichenkette '0' gleichbedeutend mit dem Wahrheitswert false ist:

console.log(false == '0'); // => true

Darüber hinaus wird die leere Zeichenkette als false interpretiert – hier greift die Transitivität jedoch nicht vollständig: Zwar sind '0' und false auf der einen Seite und false und '' auf der anderen Seite gleichbedeutend, nicht aber '0' und '':

console.log('0' == ''); // => false

Das mag auf den ersten Blick logisch erscheinen, führt aber dazu, dass zwei durchaus verschiedene Ausdrücke gleichermaßen zu false konvertiert werden und daher unter Umständen doch als gleich gelten.

Ausgesprochen interessant ist das Verhalten von JavaScript bei den beiden Werten null und undefined, die für ein unbekanntes Objekt beziehungsweise eine nicht initialisierte Variable stehen: Sie werden im direkten Vergleich mit den Werten false, 0 und '' als verschieden angesehen, wie das folgende Codebeispiel zeigt:

console.log(false == null); // => false
console.log(false == undefined); // => false

Allerdings gelten sie gegenseitig als identisch:

console.log(undefined == null); // => true

Überraschenderweise werden beide dennoch als false interpretiert, wenn man sie als Bedingung im Rahmen einer Schleife oder einer Abfrage verwendet:

var foo = undefined;
if(foo) {
console.log('Diese Zeile wird nie ausgeführt ...');
} // => false

Diesem Wirrwarr lässt sich auf zwei Arten begegnen: Entweder indem man ihm aus dem Weg geht oder indem man sich dieses spezielle Verhalten zu Nutze macht. Die erste Wahl lässt sich ausgesprochen leicht umsetzen: Beim Einsatz des ===- anstelle des ==-Operators wird zusätzlich zum eigentlichen Wert beziehungsweise der Referenz auch der jeweilige Typ berücksichtigt, und JavaScript verhält sich genauso wie die C-Sprachen:

console.log(0 === ''); // => false
console.log(0 === '0'); // => false
console.log(0 === false); // => false
console.log(null === undefined); // => false

Analog zum !=-Operator gibt es den !==-Operator, der auf Ungleichheit unter Berücksichtigung des Typs prüft:

console.log(0 != '0'); // => false
console.log(0 !== '0'); // => true

Als Faustregel lässt sich festhalten, dass man stets die typsicheren Operatoren === und !== verwenden sollte, um durch die implizite Konvertierung hervorgerufene und daher gut versteckte logische Fehler zu verhindern. Dennoch gibt es Szenarien, in denen die implizite Konvertierung gewünscht sein kann. Häufig ist beispielsweise bei Zeichenketten zu prüfen, ob sie einen Wert enthalten, bevor eine Verarbeitung angestoßen wird. Während in C# Konstrukte wie

if(foo != null && foo.Length > 0) { ... }

oder

if(String.IsNullOrWhitespace(foo)) { ... }

erforderlich sind, genügt es in JavaScript, die zu prüfende Zeichenkette als Bedingung anzugeben:

if(foo) { ... }

Auch die Existenz von Objekteigenschaften lässt sich auf dem Weg vornehmen. Da JavaScript beim Zugriff auf eine nicht vorhandene Eigenschaft den Wert undefined zurückliefert, lässt sich auf einfache Weise deren Existenz prüfen und entsprechend handeln:

if(customer.middleName) { ... }

Zu guter Letzt machen sich auch die beiden logischen Operatoren || und && das Verhalten des Typsystems zu Nutze. Anders als ihre Pendants in den C-Sprachen geben sie nämlich keinen Wahrheitswert zurück, sondern einen der beiden zu verknüpfenden Werte. Der ODER-Operator || liefert im Fall, dass sich der erste Operand zu true evaluieren lässt, diesen zurück, ansonsten den zweiten:

var a = 23,
b = 42,
result = a || b;

Die Variable result enthält in dem Fall den Wert 23. Beim Verwenden dieses Ausdruck in einer Bedingung ergibt sich genau der gewünschte Effekt: Die Bedingung wird zu true evaluiert, da 23 ungleich 0 gilt und nur die Zahl 0 zu false ausgewertet wird.

Als Nebeneffekt lässt sich der ||-Operator aber auch für einen anderen Einsatzzweck verwenden: den Fallback auf einen Standardwert. Beispielsweise könnte man beim Auslesen von Konfigurationsdaten zunächst den Wert des Benutzers nutzen. Falls dieser jedoch nicht gesetzt wird, verwendet man einen geeigneten Standardwert:

var userValue = getUserValue(),
value = userValue || 23;

Analog funktioniert auch der &&-Operator, mit der Ausnahme, dass sich dessen Logik umgekehrt verhält: Lässt sich der erste Operand zu true evaluieren, gibt er den zweiten Operanden zurück, ansonsten den ersten.

Das Typsystem von JavaScript wirkt auf den ersten Blick unscheinbar und harmlos, birgt jedoch etliche Besonderheiten. Für einen Entwickler, der eine andere Sprache gewöhnt ist, stellen diese durchaus eine große Gefahr dar, schließlich ist es ein Leichtes, versehentlich nur schwer auffindbare logische Fehler einzubauen. Wer sich jedoch auf JavaScript und dessen Besonderheiten einlässt, wird mit ausgesprochen mächtigen Sprachfunktionen belohnt, die in vielen Fällen einiges an Tipparbeit sparen können. Das führt zu kompakterem, besser lesbarem und letztlich zu verständlicherem Code.

Als Zusammenfassung kann gelten, dass Vergleiche prinzipiell ausschließlich mit typsicheren Operatoren === und !== ausgeführt werden sollten. Dennoch hat die implizite Konvertierung ihre Daseinsberechtigung und lässt sich insbesondere zur eleganten Prüfung der Existenz von Objekten und für den kompakten Fallback auf Standardwerte nutzen.

Golo Roden
ist freiberuflicher Wissensvermittler und Technologieberater für Webentwicklung, Codequalität und agile Methoden.
(ane)