| E-Mail: |
|---|
Bei Fragen zu diesem Beitrag bitte den Autor des Beitrags kontaktieren!
Zeitgemäße JavaScript sollen robust und browserübergreifend funktionieren, mit anderen Scripten problemlos zusammenarbeiten und unter Umständen von fremden Entwicklern angesteuert werden. Bis vor einiger Zeit wurden zu diesem Zweck zuhauf zweifelhafte »Browserweichen« eingesetzt, die z.B. mithilfe von des
navigator-Objektes versuchten, den Browser anhand seines Namens zu identifizieren. Diese Erkennung wurde dann verwendet, um indirekt auf die Fähigkeiten des Browsers zu schließen.
Dieser Weg ist in der heutigen Weblandschaft, in der die Browser in ihren verschiedenen Versionen unzählige Techniken unterschiedlich umsetzen, nicht mehr gangbar. Der Browsermarkt und die Entwicklung neuer, in JavaScript nutzbarer Techniken ist zu stark in Bewegung, als dass eine solche »Browserweiche« zuverlässig und zukunftsfähig sein könnte. An deren Stelle sind »Fähigkeitenweichen« getreten. Diese fragen nicht allgemein den Namen des Browsers ab und versuchen Rückschlüsse auf die Unterstützung gewisser Techniken, sondern prüfen ganz konkret die Existenz derjenigen JavaScript-Objekte, die tatsächlich im Laufe des Scriptes verwendet werden. Entscheidend ist dann lediglich, ob die nötigen Objekte existieren und korrekt funktionieren, das heißt z.B., dass Eigenschaften einen gewissen erwarten Wert haben oder Methoden den spezifizierten Rückgabewert ergeben.
Unter anderem aus diesen Gründen spielt die Abfrage von Objekten heutzutage eine zentrale Rolle in JavaScript. Leider sieht eine Objektabfrage im Einzelfall sehr unterschiedlich aus. Welche Abfrage in welchem Fall die effektivste und zuverlässigste ist, erschließt sich nicht ohne Weiteres. Dieser Artikel soll die Grundlagen einer Objektabfrage erklären und anschließend die verschiedenen verbreiteten Objektabfragen diskutieren. Ziel ist es, die Funktionsweise und die geeigneten Anwendungen verschiedener Abfrage-Techniken zu vermitteln.
Objektabfragen haben drei Ebenen:
Diese Abfragen müssen manchmal gestaffelt sein: Denn wenn ein Objekt gar nicht existiert, ist unter Umständen Vorsicht geboten bei der Abfrage des Types. Und die Abfrage des Wertes schließt ein, dass ein bestimmter Typ vorausgesetzt wird. Am Anfang steht daher die Überlegung, welche dieser Ebenen überhaupt geprüft werden soll.
Unter Abfragen verstehen wir hier vor allem bedingte Verzweigungen mit if (...) { ... } else { ... }. Im engeren Sinne werden wir uns mit der Bedingung beschäftigen, die bei der if-Anweisung zwischen den runden Klammern notiert wird. Dieselben Bedingungen finden aber auch bei anderen
bedingten Anweisungen, nämlich dem sogenannten konditionalen Operator und bei switch-Fallunterscheidungen Verwendung.
Diese Bedingung ist ein beliebiger JavaScript-Ausdruck (»Expression«), welcher einen Boolean-Wert ergeben muss - das heißt true oder false. Der JavaScript-Interpreter erwartet also an dieser Stelle einen Boolean-Wert und wandelt das Ergebnis des Ausdrucks zwischen den Klammern automatisch in den Typ Boolean um, sofern es einen anderen Typ hat. Diese Umwandlung gehorcht festen Regeln, die wir nun kennenlernen werden.
Was ergibt also in Boolean umgewandelt true? Anders herum gefragt: Welche Werte ergeben in Boolean umgewandelt false?
false ergibt false.null und undefined ergeben false0 und NaN ergeben false"") ergibt falseAll anderen Werte ergeben true.
Das bedeutet: Vordefinierte Objekte wie window, window.document usw., Funktionsobjekte, DOM-Knoten-Objekte, darunter Elementobjekte, sowie Array, Date- und RegExp-Objekte uvm. ergeben allesamt true. Kurz gesagt ergeben nicht-leere Strings, Zahlen ungleich 0 bzw. NaN und »richtige« Objekte (Objects, dazu später mehr) true.
(Übersicht entnommen aus:
Peter-Paul Koch: ppk on JavaScript. New Riders, Berkeley 2007. S. 160)
if (objekt)Prüft, ob eine globale oder lokale Variable nach Boolean umgewandelt true ergibt. Wenn weder eine lokale noch eine globale Variable dieses Namens existiert, bricht das Script hier mit einem Ausnahmefehler (einer sogenannten Exception) ab: ReferenceError: objekt not defined.
Eignet sich zur Abfrage, ob einer deklarierten lokalen Variable ein Wert zugewiesen wurde, der bei der Umwandlung in Boolean true ergibt. Deklariert bedeutet, dass var objekt; notiert wurde oder die Variable in der Parameterliste der Funktion aufgeführt ist, ohne dass ein Wert für sie übergeben wurde. objekt hat dann den speziellen Wert undefined, was umgewandelt gleich false ergibt.
Beachten Sie: Eignet sich nicht ohne Einschränkungen zum Überprüfen, ob einer deklarierten Variable überhaupt ein Wert zugewiesen wurde. Denn wie oben aufgelistet ergeben ein leerer String (""), der Boolean-Wert false sowie null ebenfalls false.
if (objekt == true)Lange Schreibweise von if (objekt). Kann zugunsten von if (objekt) vermieden werden, denn jenes hat genau denselben Effekt. Der Ausdruck wird wie gesagt ohnehin automatisch in Boolean umgewandelt, sodass ein ausdrücklicher Vergleich mit true unnötig ist.
if (objekt == undefined)Genauere Überprüfung, ob eine globale oder lokale Variable deklariert wurde, ihr aber kein Wert zugewiesen wurde. Wenn ein Funktionsparameter in der Parameterliste aufgeführt ist, aber kein Wert übergeben wurde, existiert eine entsprechende lokale Variable ohne Wert.
Hat die Variable den speziellen Wert null, so ergibt der Vergleich ebenfalls true. Um dies zu vermeiden, können Sie if (objekt === undefined) notieren. Der Identitätsoperator === überprüft sowohl die Gleichheit des Typs (hier Undefined) als auch die Gleichheit des Wertes (hier undefined).
Wenn keine Variable dieses Namens existiert, bricht das Script an dieser Stelle mit einem Ausnahmefehler (einer Exception) ab: ReferenceError: objekt not defined.
if (objekt == wert)Beispielsweise if (objekt == "string") oder if (objekt == 1234) usw. Genaue Überprüfung von Typ und Wert (im Beispiel String bzw. Number) von globalen oder lokalen Variablen.
Damit die Vergleiche die erwarteten Resultate bringen, sollte objekt bereits vom jeweiligen Typ sein. Ansonsten erfolgt eine automatische Umwandlung, im Beispiel in die Typen String bzw. Number.
Wenn keine Variable dieses Namens existiert, bricht das Script an dieser Stelle mit einem Ausnahmefehler (einer Exception) ab: ReferenceError: objekt not defined.
if (objekt.eigenschaft) und if (objekt["eigenschaft"])Prüft, ob ein Objekt eine Eigenschaft (ein Unterobjekt) besitzt, die in Boolean umgewandelt true ergibt.
Wenn das Objekt kein Unterobjekt dieses Namens besitzt, bricht das Script an dieser Stelle nicht mit einem Ausnahmefehler ab, sondern der Ausdruck gibt ergibt einfach undefined. undefined ergibt wie gesagt in Boolean umgewandelt false, damit würde der if-Anweisungsblock einfach nicht ausgeführt. (Sie können aus denselben Gründen if (objekt.eigenschaft == wert) verwenden, um auf einen bestimmten Wert zu prüfen, ohne dass das Script abbricht, wenn die Objekteigenschaft nicht existiert.)
Dies ist die beste Methode für »Fähigkeiten-Weichen«, die abfragen, ob vordefinierte Objekte (bzw. Eigenschaften oder Methoden) im jeweiligen Browser zur Verfügung stehen. Besonders für die Existenzabfrage von Methoden eignet sich diese Schreibweise. Ein Beispiel:
if (document.getElementById) {
var elem = document.getElementById("a");
if (elem.scrollIntoView) {
elem.scrollIntoView();
}
}
Existieren die Methoden getElementById bzw. scrollIntoView, werden sie verwendet, anderweitig wird der entsprechende Code übersprungen.
Interessant ist ferner, dass sich auf diese Weise auch durchaus globale Variablen überprüfen lassen. Denn globale Variablen sind in JavaScript nichts anderes als Eigenschaftes des globale Objektes window. Also kann man einfach if (window.globaleVariable) bzw. if (window["globaleVariable"]) notieren. Der Vorteil gegenüber if (globaleVariable) ist, dass das Script nicht mit einem Ausnahmefehler abbricht, wenn selbige Variable nicht existiert.
if (typeof objekt == "typ") und if (typeof objekt.eigenschaft == "typ")Mit dem typeof-Operator lässt sich abfragen, ob eine Variable bzw. eine Objekteigenschaft einen bestimmten Typ hat. typeof gibt einen String zurück, je nach Typ des Operanden gemäß der folgenden Tabelle:
| Typ des Operanden | typeof-Rückgabe |
|---|---|
| Undefined | "undefined" |
| Null | "object" |
| Boolean | "boolean" |
| Number | "number" |
| String | "string" |
| Funktionsobjekte | "function" |
| sonstige Objekte (»Objects«, siehe unten) | "object" |
typeof in JavaScript verhält sich ganz anders als entsprechende Operatoren bzw. Funktionen in anderen Programmiersprachen. Das hat damit zu tun, wie JavaScript intern organisiert ist. Zum Verständnis von typeof ist ein kleiner Exkurs nötig:
JavaScript (ECMAScript) behandelt zwar alles als Objekt, macht eine Unterscheidung zwischen sogenannten Primitives (einfachen Werten) und Objects (»richtigen«, vollwertigen Objekten). Diese Doppelung betrifft Boolean-, Number- und String-Werte, diese können als Primitive oder als Object notiert werden. Üblicherweise arbeitet man mit Primitives: var string = "Hallo Welt" sowie die üblichen String-Operationen erzeugen String-Primitives, während var string = new String("Hallo Welt"); ein String-Object erzeugt. Dieser Unterschied macht sich vor allem an zwei Stellen bemerkbar:
== ergibt beim Vergleich zweier Objects nur dann true, wenn es sich um ein und dasselbe Object handelt (er verhält sich in dem Fall wie der Identitätsoperator ===). Das bedeutet, dass new String("Hallo Welt") == new String("Hallo Welt") kurioserweise false ergibt. Bei Primitives hingegen gibt es eine Gleichheit unabhängig von der Identität: "Hallo Welt" == "Hallo Welt" ergibt selbstverständlich true.Was hat das nun mit typeof zu tun? typeof unterscheidet im Grunde nur zwischen undefinierten Werten (Undefined), den drei Primitives (Boolean, Number, String) und Objects (Funktionen vs. alle anderen Objects). Was nicht definiert, kein Primitive und kein Funktionsobjekt ist, klassifiziert typeof zusammenfassend als "object". Dadurch gibt typeof verwirrenderweise sehr häufig "object" zurück, was vielen unverständlich ist. Zum Beispiel Array-, Date- oder RegExp-Objekte werden als "object" klassifiziert - diese sind aus Sicht von typeof kein eigenen Typen. Aber auch der Wert null wird als "object" klassifiziert, obwohl es sich strenggenommen um ein Primitive handelt - dies kann einige Verwirrungen stiften kann.
Dummerweise setzen nicht alle Browser diese Vorgaben der obigen Tabelle konsequent um, sodass typeof in verschiedenen Browsern z.B. bei gleichen vordefinierten Objekten unterschiedliche Resultate erzeugt. In manchen Browsern identifiziert typeof ein vordefiniertes Funktionsobjekt fälschlicherweise als "object" anstatt als "function".
Diese Einschränkungen und Fallstricke führen dazu, dass eine Abfrage typeof nur in bestimmten Fällen das Mittel der Wahl ist. Verwenden Sie typeof möglichst nur bei einfachen Werten (Boolean-, Number- und String-Primitives) und nur bei selbst definierten Funktionen. Achten Sie auf mögliche Browserunterschiede und die Besonderheiten bei der Klassifizierung.
Nichtsdestoweniger ist typeof die zweiwichtigste Abfragetechnik in der browserübergreifenden Programmierung neben if (objekt.unterobjekt). Der Vorteil ist, dass nur der Typ überprüft wird, nicht der Wert selbst in Boolean umgewandelt und auf Gleichheit mit true getestet wird - wie es z.B. bei if (objekt.unterobjekt) der Fall ist. Wenn bloß die Existenz einer Variable geprüft werden soll, es sich aber beispielsweise durchaus um einen leeren String, die Zahl 0 oder den Boolean-Wert false handeln darf, eignet sich typeof sehr gut.
Abfragen mit typeof sind wie gesagt weniger »scharf« als if (objekt.unterobjekt), da der Wert selbst keiner Boolean-Prüfung unterzogen wird. Dennoch sind die beiden Möglichkeiten in vielen Fällen austauschbar. Oft ist es eine Geschmackfrage, welche Methode verwendet wird. Meine Empfehlung ist, häufiger if (objekt.unterobjekt) zu verwenden und typeof speziell dann zu verwenden, wenn der Inhalt egal ist, solange der Typ stimmt.
Strenge Typabfragen können verwirrend sein, da JavaScript flexibel mit Typen umgeht, um die Programmierung zu vereinfachen. Je nach Kontext werden Werte bei der Code-Ausführung automatisch in andere Typen konvertiert, z.B. damit Operanden und Funktionen mit den erwarteten Typen arbeiten können. Dies ist vielmals erwünscht und findet breite Verwendung, ohne dass JavaScript-Programmierer sich dessen notwendigerweise bewusst sind. Es ist nicht immer problematisch, wenn eine Variable z.B. anstelle des Wertes 250 vom Typ Number den Wert "250" vom Typ String enthält. Es kann aber ein großes Problem werden. Etwa variable.toFixed() funktioniert nur bei Number-Werten und der Operator + hat eine ganz andere Bedeutung, wenn ein Operand ein String ist.
Wenn Sie also typeof benutzen, um einen speziellen Typ abzufragen, sollten Sie sich der Möglichkeiten der automatischen und manuellen Typen-Konvertierung bewusst sein. Benutzen Sie etwa
parseInt oder
parseFloat zur Umwandlung von Strings in Zahlen und
toString zur expliziten Umwandlung von Zahlen in Strings. So können Sie robuste, tolerante Programme schreiben:
function Beispiel (parameter) {
var zahl;
if (typeof parameter == "string") {
zahl = parseFloat(parameter);
if (zahl == NaN) {
// Umwandlung in String ergab keinen brauchbaren Wert.
return false;
}
} else (typeof parameter == "number") {
zahl = parameter;
} else {
// Es wurde kein brauchbarer Wert übergeben.
return false;
}
// Arbeite mit zahl weiter...
}
Wenn keine Variable bzw. Eigenschaft dieses Namens existiert, bricht das Script an der Stelle der Verwendung des typeof-Operators nicht mit einem Ausnahmefehler ab, sondern typeof liefert den String "undefined" zurück.
if (typeof objekt != "undefined") und if (typeof objekt.eigenschaft != "undefined")Prüft, ob eine lokale oder globale Variable bzw. eine Objekteigenschaft dieses Namens existiert und zudem einen Wert besitzt (der nicht vom Typ Undefined ist).
Aufgrund der beschriebenen Eigenheiten und Browser-Probleme wird typeof häufig in diesem negativen Sinne gebraucht. Anstatt abzufragen, ob ein Objekt einen bestimmten Typ hat, wird bloß abgefragt, ob es existiert und irgendeinen anderen Typ als Undefined besitzt. Damit kann beispielsweise die bloße Existenz vordefinierter Objekte in Erfahrung gebracht werden oder geprüft werden, ob ein Funktionsparameter gesetzt wurde, unabhängig davon, welchen Typ und welchen Wert dieser hat.
Der negative Vergleich mit "undefined" ist zwar ungenauer als die Prüfung auf einen oder mehrere bestimmte Typen, erfüllt aber oft trotzdem den gewünschten Zweck. Diese Methode findet an ähnlichen Stellen Verwendung wie if (objekt.unterobjekt).
if (objekt == null) und if (objekt.unterobjekt == null)Identisch mit if (!objekt) bis auf den Fall, in dem objekt den speziellen Wert null hat.
Diese Abfrage in in keinem Fall vorzuziehen. In den allermeisten Fällen ist sie durch if (!objekt) zu ersetzen.
Auch in dem Fall, dass überprüft werden soll, ob die Variable exakt den Wert null enthält, erzeugt sie irreführende Resultate, da auch andere Werte gleich null, aber nicht identisch mit null sind. Wenn Sie wissen wollen, ob eine Variable null enthält, verwenden Sie object === null. Der Identitätsoperator === überprüft sowohl den Typ (hier Null) als auch den Wert (hier null).
if ("eigenschaft" in objekt)Diese relativ unbekannte und selten verwendete Abfragemöglichkeit prüft, ob das Objekt eine Eigenschaft mit dem angegebenen Namen besitzt. Weder Typ und Wert werden dabei überprüft. Der Ausdruck ergibt direkt true oder false. Entspricht in jedem Fall dem bekannteren if (typeof objekt.eigenschaft != "undefined").
Der Eigenschaftsname kann hier direkt als String angegeben werden oder als String-Variable, die den Namen der zu prüfenden Eigenschaft enthält. In diesem Fall notiert man if (stringVariable in objekt), was dann if (typeof objekt[stringVariable] != "undefined") entspricht.
Der in-Operator ist sonst nur bei for-in-Schleifen bekannt. Dort hat er allerdings eine ganz andere Bedeutung und führt dazu, dass der Variablen auf der linken Seite nacheinander die Eigenschaftsnamen zugewiesen werden, damit für jede Eigenschaft der Schleifenkörper ausgeführt werden kann.
Wenn keine keine Eigenschaft mit dem angegebenen Namen existiert, bricht das Script an der Stelle der Verwendung des in-Operators nicht mit einem Ausnahmefehler ab, sondern liefert einfach den Boolean-Wert false zurück.
try { /* direkt objekt benutzen */ } catch (e) {}Das
try-catch-Statement dient eigentlich dazu, JavaScript-Ausnahmefehler (Exceptions) abzufangen und sie im catch-Zweig selbst zu behandeln, um punktuell auf einen aufgetretenen Fehler, der sonst zu, Abbruch der Scriptausführung geführt hätte, angemessen zu reagieren.
Dieses Konzept ist eigentlich sehr brauchbar. Allerdings gibt es zwei Probleme, sowohl grundlegender als auch praktischer Art.
Zum ersten wird try-catch in den wenigsten Fällen sinnvoll verwendet. Oft wird der catch-Zweig komplett leergelassen, bloß um eventuelle Exceptions und damit mögliche störende und verwirrende Meldungsfenster im Browser zu unterdrücken, anstatt sie zu behandeln. try-catch wird in dieser Weise inflationär gebraucht, anstatt zielgenaue Objektabfragen bzw. tatsächliche Fehlerbehandlung einzusetzen.
Zum zweiten ist es mit try-catch - insbesondere, wenn große Programmteile in try-Blöcke untergebracht werden - nicht möglich, spezifisch auf einen Fehler zu reagieren. Das Fehlerobjekt, auf das man im catch-Block Zugriff hat, enthält wenig brauchbare Informationen. Die Fehlermeldung kann höchstens abgefangen werden, um sie beispielsweise mit XMLHttpRequest an den Server zurückzusenden. Dort könnten alle Fehlermeldungen gespeichert werden, um Scripte zu verbessern, damit sie in Zukunft keine Exceptions mehr produzieren.
Es gibt nur wenige Anwendungsfälle, wo try-catch nötig oder nützlich ist. Der Einsatz von try-catch zur Fehlerunterdrückung führt während der Entwicklung dazu, dass eventuelle Scriptfehler nicht rechtzeitig erkannt werden. Robuster, fehlertoleranter JavaScript-Code sollte im Produktiveinsatz auch in Sonderfällen keine unerwarteten Exceptions werfen. Daher erwähnt dieser Artikel ausdrücklich, welche Abfragetechniken Exceptions auslösen und welche »sicher« sind, auch wenn die angesprochene Variable bzw. Eigenschaft nicht existiert. Diese »sicheren« Objektabfragen können vermeiden, dass JavaScript-Exceptions überhaupt erst auftreten.
Es gibt noch viele weitere Argumente gegen einen häufigen Einsatz von try-catch: Beispielsweise ist die Ausführung viel langsamer als die anderen hier beschriebenen zielgenaueren Abfragen. Zusammenfassend: Verwenden Sie try-catch nur im Notfall, wenn andere Objektabfragen nicht möglich sind oder nicht greifen.
try-catch ist unvermeidlich und sinnvoll, falls im try-Block eine ganz bestimmte Anweisung steht, von der erwartet wird und für die spezifiziert ist, dass sie bei der regulären Verwendung eine Exception auslösen kann. Doch nur einige besondere JavaScript-Operationen erzeugen im regulären Einsatz Exceptions, z.B. die Erzeugung von ActiveX-Objekten wie XMLHttpRequest im Internet Explorer. (Dies ist nur bei Versionen kleiner als 7 nötig, ab Version 7 existiert das Objekt window.XMLHttpRequest.)
try-catch soll keinesfalls verdammt werden. Sein Einsatz ist im durchschnittlichem JavaScript im WWW zum Zwecke der Fehlerunterdrückung lediglich selten sinnvoll, weil genauere und bessere Alternativen für robuste Scripte existieren. Es spricht allerdings nichts gegen den kontrollierten Einsatz von try-catch und etwa throw in eigenen Programmen, um die bestimmte Ausnahmen auf einer anderen Ebene zu behandeln, als dort, wo sie auftreten. Es spricht auch nichts dagegen, try-catch im Sinne einer spezifischen Fallunterscheidung zu gebrauchen, die im catch-Zweig Alternativen anwendet, um die Aufgabe trotz des Fehlers auf eine andere Weise zu lösen.
© 2006
Impressum, für diese Seite:
molily@selfhtml.org