4. JavaScript

Dieses Kapitel beschreibt die Grundlagen und Entwurfsmuster von JavaScript. Dies ist eine kompakte Einführung, die dabei helfen soll, Beispiele zu Umgebungen wie NodeJs oder Angular zu verstehen und eigene Entwicklungen umzusetzen. Das Kapitel verzichtet auf umfassende geschichtliche Ausflüge zugunsten relevanter Inhalte. Behandelt wird ECMAScript 5.

Diese Version ist meist auch die Zielsprache für TypeScript. TypeScript ist eine typisierte Sprache, die die Programmierung erheblich vereinfacht und die in JavaScript kompiliert wird. Mehr dazu im nachfolgenden Kapitel.

4.1 Einführung

JavaScript ist eine relativ kompakte und mächtige Skriptsprache, welche um das Jahr 1995 von Netscape eingeführt wurde, um Webseiten mit Interaktionen ausstatten zu können. Seitdem sind 20 Jahre vergangen und JavaScript ist aus so gut wie keiner Webanwendung mehr weg zu denken. Dabei spielt es keine Rolle, mit welcher Technologie diese Webanwendung realisiert wurde. Je nach Hersteller des Browsers kann die Sprachimplementierung leicht variieren. Die häufigste Implementierung ist jedoch in Browsern zu finden, wo die Sprache die Grundlage für die interaktiven Elemente von HTML 5 liefert. Grundsätzlich ist JavaScript aber unabhängig vom Browser. Die Benutzung in Node ist eine vom Browser vollständig entkoppelte Implementierung der Sprache basierend auf der V8-Engine von Google, die auch den Browser Chrome antreibt.

Trotz des sehr ähnlich klingenden Namens und der teilweisen syntaktischen Ähnlichkeit zu JAVA ist JavaScript eher nicht mit der Programmiersprache JAVA verwandt. Bis auf die Ausnahme, dass beide Programmiersprachen sich in ihrer Syntax sehr an der Programmiersprache C orientiert haben.

Ursprünglich war JavaScript ausschließlich zu dem Zweck gedacht, den DOM-Baum (Document Object Model) des Browsers zur Laufzeit zu manipulieren. Jedoch entwickelten sich mit der Zeit immer mehr Anwendungsgebiete für diese universelle Skriptsprache. So gibt es bereits erste Referenzimplementierungen ganzer Anwendungsprogramme in JavaScript, beispielsweise basierend auf nw.js.

4.2 Sprachmerkmale und Entwurfsmuster

JavaScript als Programmiersprache wurde von einem der Erfinder folgendermaßen beschrieben:

JavaScript is a C-family, world’s worst named, extremely powerful language (not a script), totally unrelated to Java.

Was ist das Problem mit JavaScript? Einige Zitate (im Original) helfen beim Verständnis:

Vergleich mit Programmiersprachen

Die Kernideen von JavaScript sind:

Die Sprachelemente umfassen:

4.3 JavaScript-Syntax

Die Namen für Variablen und Funktionen beginnen mit einem Buchstaben, _ oder $ (In “Regex-Speak”: [a-z,A-Z,_,$]), gefolgt von keinem oder mehreren Buchstaben, Zahlen, _ oder $. Das $ hat keine spezielle Bedeutung, sollte generiertem oder Bibliotheks-Code vorbehalten bleiben. Alle Variablen, Parameter oder Member werden klein geschrieben (meinVariablenName). Einzige Ausnahme: Konstruktoren beginnen mit einem großen Buchstaben (MeinKlassenKonstruktor). Das _-Zeichen (Unterstrich) am Beginn eines Schlüsselwortes wird für Implementierungen verwendet (privat per Konvention).

Das einfachste Beispiel, “Hello JavaScript”, zeigt die Nutzung auf einer einfachen HTML-Seite:

Listing: Hallo JavaScript
 1 <html>
 2 <head>
 3 <title>Hello JavaScript</title>
 4 <script type="text/javascript">
 5 function sayHelloTo(name) {
 6 document.write("Hallo " + name + " !");
 7 }
 8 </script>
 9 </head>
10 <body>
11 
12 <script type="text/javascript">
13 document.write("Hallo  JavaScript !\n");
14 sayHelloTo("Joerg");
15 </script>
16 
17 </body>
18 </html>

JavaScript-Code kann an den unterschiedlichsten Stellen einer Webseite verwendet werden. Die zwei gebräuchlichsten davon sind der Kopfbereich (Zeile 4) als Codeblock, welcher meistens für die Definition von Funktionen verwendet wird, sowie innerhalb des Bodys einer Seite Ende (Zeile 12ff.).

Grundsätzlich wird der auszuführende Code von einem entsprechenden Script-Tag eingeschlossen, welcher mindestens den Typ text/javascript als Mime-Typ enthalten muss.

Kommentare

Ein kommentierter Bereich sieht folgendermaßen aus:

1 /*
2 Ein mehrzeiliger Kommentarblock
3 Kann mit den aus C/C++/C#/Java bekannten Blockkommentarzeichen
4 erstellt werden.
5 */

Der Zeilenkommentar steht alleine auf einer Zeile oder am Ende:

1 // Dies tut das:
2 var i = 42; // Zuweisung

Literale

Die folgende Tabelle zeigt Literale für Sonderzeichen:

Tabelle: Sonderzeichen
Zeichen Bedeutung
\b BackSpace
\n NewLine
\t Tab
\f FormFeed
\r CarriageReturn

Ein Beispiel dazu:

1 'Das ist eine mehrzeilige\r\nZeichenkette'

Umlaute

Eine Besonderheit ist beim Umgang mit Umlauten zu beachten. Es ist nicht sichergestellt, dass die Ausgabe mit den Sonderzeichen umgehen kann. Aus diesem Grund ist es empfehlenswert, auf die Funktion unescape zurückzugreifen. Dies sieht mit Umlauten folgendermaßen aus:

1 alert("über"));

Schreibe jedoch folgendes, wenn Umlaute benutzt werden:

1 alert(unescape("%FCber"));

Damit stellst du sicher, dass die Umlaute immer korrekt interpretiert und angezeigt werden können.

Numerische Literale

Ferner gibt es noch die numerischen Literale. JavaScript unterscheidet im Wesentlichen in Ganzzahlen und Gleitkommazahlen bei den Literalen. Intern werden nur Gleitkommazahlen abgebildet. Ganzzahlen können in folgenden drei Formen vorkommen:

Gleitkommazahlen können in zwei Schreibweisen vorkommen:

Ferner gibt es zur Darstellung von Wahrheitswerten noch boolesche Konstanten:

Sonstige Literale

Reguläre Ausdrücke werden in / (slash) geschrieben. Das Array-Literal [] wird für Arrays und Maps benutzt. Das Objekt-Literal {} erzeugt ein neues, leeres Objekt.

Operatoren

Das Zeichen + dient sowohl der Addition als auch der Verknüpfung von Zeichenketten. Wenn beide Operanden Zahlen sind, werden diese addiert, sonst erfolgt immer eine Umwandlung in Zeichenketten, welche zusammengesetzt werden.

Tabelle: Mathematische Operatoren
Operator Bedeutung Beispiel
+, += Addition x+=3
-, -= Subtraktion x=x-5
*,*= Multiplikation a=b*c
/, /= Division z=e/5
% Modulus m=5 % 3
++, -- Inkrement, Dekrement x++ oder y–
<<, <<= Bitweise Linksschieben x << 4
>>, >>= Bitweise Rechtsschieben y >> 5
>>> Bitweise Linksschieben mit Nullfüllung a >>> b
& Bitweise UND a & b
| Bitweise ODER a | b
^ Bitweise Negieren ^b

4.4 Typen

JavaScript ist “schwach typisiert” (loosely typed). Keineswegs kann man hier von einer untypisierten Sprache sprechen. Wird einer Eigenschaft oder einer Variablen ein Wert zugewiesen, so bekommt die Variable den Typ des jeweiligen Wertes. Intern werden dennoch Typen unterschieden. Elementar sind:

Dazu kommen die folgenden speziellen “Zustände”:

Des weiteren sind einige Typen eingebaut, um alltägliche Aufgaben zu erleichtern:

Diese Typen basieren alle auf Object.

Automatische Typkonvertierung

Wird die Konvertierung nicht explizit angegeben, versucht JavaScript den am besten passendsten Typ zu ermitteln. Durch verschiedene Operatoren können können Umwandlungen elegant erzwungen werden. Dazu mehr im Abschnitt Operatoren.

Number

Number ist der einzige Zahlentyp, 64Bit Gleitkomma (floating point). Nach IEEE-754 ist dies double.

Ein Spezialwert ist NaN – Not a Number. Dieser entsteht als Ergebnis fehlerhafter Berechnungen. Jede Berechnung mit einem Parameter NaN ergibt NaN. NaN ist ungleich zu allem, auch zu NaN selbst. Der folgende Vergleich ergibt immer false:

1 if (NaN == NaN) {
2   // Dieser Teil wird niemals erreicht
3 }

Testen kann man dies mit Number.isNaN(value). Die Klasse Number bietet einige solche explizite Funktionen. Eine Instanz wird folgendermaßen erstellt:

1 var n = Number(value);

Dies konvertiert einen Wert in eine Zahl. Im Fehlerfall entsteht NaN. Konvertiert werden Zahlen besser durch Operatoren. Mit + sieht das dann folgendermaßen aus:

1 var z = "23";
2 var n = +z;

Das Plus-Zeichen wird hier als nicht verändernden Präfix-Operator benutzt. Dies erzwingt die Umwandlung in eine Zahl.

Die Funktion parseInt(value, radix) verwandelt in eine Ganzzahl mit einer definierten Zahlenbasis (bei Dezimalzahlen ist die Basis 10). Im Fehlerfall ensteht wieder NaN:

1 var i = parseInt("1F", 16); // enspricht 31
Berechnungen mit Math

Das Math-Objekt enthält Konstanten und Funktionen zur Berechnung. Es können auch komplexe Berechnungen wissenschaftlicher oder kaufmännischer Art durchgeführt werden. Die Sprachspezifikation kann als Referenz zusätzlich empfohlen werden.

Eine Instanz von Math braucht nicht eigens erzeugt werden. Eigenschaften und Methoden von Math können direkt verwendet werden. Das Schema sieht folgendermaßen aus:

1  x = Math.Eigenschaft;
2  x = Math.Methode(Parameter);

Mit Zahl = 10 * Math.PI beispielsweise steht in der Variablen Zahl nach der Zuweisung das Produkt aus der Zahl π und 10. Mit Wurzel = Math.sqrt(10) steht in der Variablen Wurzel hinterher das Ergebnis der Quadratwurzel aus 10. Vor jedem Aufruf einer Eigenschaft oder Methode des Math-Objekts wird Math benutzt (großgeschrieben).

Bei jedem Zahlen-Parameter, den du einer Methode von Math übergibst, kann es sich um eine explizite Zahl (z.B. 25 oder 0.123) handeln, um eine numerische Variable (z.B. x) oder um einen Rechenausdruck (z.B. 7 * 5 + 0.3). Auch Rechenausdrücke mit Variablen sind erlaubt (z.B. x * i + 1).

Die Konstanten lauten:

Die Funktionen sind:

String (Zeichenkette)

Ein String ist eine Sequenz von 0 oder mehr 16Bit-Zeichen. In ECMAScript wurde ursprünglich UCS-2 benutzt, wobei einige Implementierungen UTF-16 benutzen. Die Ausgabe sollte immer in UCS-2 erfolgen, intern wird meist UTF-16 eingesetzt. Der Unterschied macht sich bemerkbar, wenn die Anzahl der Bytes ermittelt wird, aus denen ein Zeichen besteht.

Es gibt keinen speziellen Typ für ein Zeichen (kein char-Typ). Ein Zeichen wird durch einen String der Länge 1 repräsentiert. Zeichenketten sind unveränderlich (immutable), wie in den meisten anderen Sprachen auch. Zeichenketten gleichen Inhalts sind gleich (==), auch wenn es sich um verschiedene Objekte handelt. Dies entspricht der intuitiven Lesbarkeit.

Zeichenkettenliterale können " oder ' verwenden. Die Benutzung ist gleichwertig. Dadurch sind Verschachtelungen möglich wie "Hallo 'Welt' " oder 'Hallo "Welt"'.

Die Länge der Zeichenkette wird mit string.length ermittelt. String(value) wandelt einen Wert in eine Zeichenkettendarstellung um.

Die String-Methoden

Zeichenketten in JavaScript sind 0-basiert, d.h. das erste Zeichen hat den Index 0. Dies gilt für alle Zeichenkettenmethoden, die mit Indizes arbeiten. Zeichenketten sind darüberhinaus unveränderlich (immutable). Methoden, die Änderungen vornehmen, geben immer eine neue Instanz einer Zeichenkette zurück. Mehrfach Manipulationen sind dadurch zwar sehr schnell, führen aber auch dazu, dass dieselben Zeichen möglicherweise mehrfach im Speicher liegen. Bei großen Zeichenketten solltest du sehr sorgfältig mit den Datenmengen umgehen.

indexOf()
Mit dieser Methode wird die Position einer Zeichenkette in einer anderen Zeichenkette gefunden. Wird nichts gefunden, wird -1 zurückgegeben.
1 var str = "Bitte legen Sie 'Produkte' weg!";
2 var pos = str.indexOf("Produkte");
lastIndexOf()
Wenn mehrere Vorkommen einer Zeichenkette in einer anderen existieren, wird die die letzte Fundstelle zurückgegeben. Wird nichts gefunden, wird -1 zurückgegeben.
1 var str = "Bitte legen Sie 'Produkte' weg!";
2 var pos = str.lastIndexOf("Produkte");
search()
Mit dieser Methode wird die Position einer Zeichenkette in einer anderen Zeichenkette gefunden. Wird nichts gefunden, wird -1 zurückgegeben. Im Gegensatz zu indexOf akzeptiert search auch reguläre Ausdrücke als Argument.
1 var str = "Bitte legen Sie 'Produkte' weg!";
2 var pos = str.search(/Produkte/);

Drei Methoden dienen dazu, Teile von Zeichenketten zu bilden:

slice(start, end)
Extrahiert einen Teil einer Zeichenkette von Position start bis Position end. Wenn das Argument negativ ist, wird die Position vom Ende gezählt.
substring(start, end)
Extrahiert einen Teil einer Zeichenkette von Position start bis Position end. Negative Argumente sind nicht erlaubt. Wird das Ende end weggelassen, wird bis zum Ende der Zeichenkette extrahiert.
substr(start, length)
Extrahiert einen Teil einer Zeichenkette von Position start und dann eine Anzahl Zeichen length.
1 var str = "Apfel, Banane, Kiwi";
2 var res = str.slice(7,13);
1 var str = "Apfel, Banane, Kiwi";
2 var res = str.slice(-12,-6);

Die Ausgabe ist in beiden Fällen ‘Banane’.

1 var str = "Apfel, Banane, Kiwi";
2 var res = str.substring(7,13);
1 var str = "Apfel, Banane, Kiwi";
2 var res = str.substr(7,6);

Die Ausgabe ist auch in diesen beiden Fällen ‘Banane’.

replace(search, replace)
Diese Methode ersetzt Teile einer Zeichenkette durch eine andere:
1 str = "Bitte benutze Node!";
2 var n = str.replace("Node","AngularJS");
toUpperCase
Wandelt alle Zeichen in Großbuchstaben um.
toLowerCase
Wandelt alle Zeichen in Kleinbuchstaben um.
1 var text1 = "Hallo JavaScript!";
2 var text2 = text1.toUpperCase();
3 console.log(text2);
4 var text2 = text1.toLowerCase();
5 console.log(text2);
concat()
Diese Methode kann mehrere Argumente annehmen und kombiniert diese zu einer Zeichenkette.
1 var text1 = "Hallo";
2 var text2 = "JavaScript";
3 text3 = text1.concat("	",text2);

Manipulationen einzelner Zeichen vermeiden den Umgang mit großen Zeichenketten in manchen Fällen.

charAt(position), charCodeAt(position)
Die Methoden geben das Zeichen an der gewählten Position zurück. charCodeAt gibt dabei den Unicode des Zeichens an.
1 var str = "HELLO WORLD";
2 str.charAt(0);            // Ergibt: H
3 str.charCodeAt(0);        // Ergibt: 72

Da JavaScript keinen expliziten Zeichentyp kennt, ist die Interpretation einer Zeichenkette als Array unzulässig. Es ist zwar syntaktisch möglich, kann aber unvorhersehbare Fehler produzieren. Schreibweisen wie str[0] gelten deshalb als sogenanntes Antipattern. Sollten Arrayfunktionen sinnvoll erscheinen, konvertiere die Zeichenkette zuerst explizit in ein Array-Objekt. Am einfachsten geht das mit der Methode split.

1 var txt = "a,b,c,d,e";   // String
2 txt.split(",");          // Split on commas
3 txt.split(" ");          // Split on spaces
4 txt.split("|");          // Split on pipe

Beachte die Funktion des Trennzeichens. Wenn kein Trennzeichen angegeben wird, steht die gesamte Zeichenkette im Index 0 des Arrays. Um eine Zeichenkette zeichenweise zu zerlegen und dabei keine expliziten Trennstellen zu definieren, nutze eine leere Zeichenkette "".

1 var txt = "Hello";       // String
2 txt.split("");           // Split in characters
Nicht standardkonforme String-Methoden

Einige Methoden sind nicht vom Standard abgedeckt, werden aber von praktisch allen Browsern unterstützt. Umgebungen außerhalb des Browsers, wie beispielsweise Node, unterstützen diese Funktionen nicht.

Tabelle: Browserspezifische Funktionen
Methode Erzeugt das HTML-Element…
anchor(name) <a name="name"></a>
big() <big></big>
blink() <blink></blink>
bold() <b></b>
fixed() <tt></tt>
fontcolor(c) <font color="c"></font>
fontsize(s) <font size="s"></font>
italics() <i></i>
link(href) <a href="href"></a>
small() <small></small>
strike() <strike></strike>
sub() <sub></sub>
sup() <sup></sup>

Der Inhalt des Elements ist jeweils die Zeichenkette, auf die die Methode angewendet wurde.

Boolean

Boolesche Werte können true (Wahr, Ja) oder false (Unwahr, Nein) sein. Beides sind Literale in JavaScript.

Die Funktion Boolean(value) ermittelt, ob der Wert true oder false ist. Werte, die als false interpretiert werden, sind:

Alle anderen Werte sind true, einschließlich "0" und "false" (in Anführungszeichen), was manchmal zu Verwirrung führt.

4.5 Objekte

Objekte in JavaScript sind dynamisch. Dies ist eine Mischung aus einem Objekt und einem Dictionary (Map). Mit new Object() erzeuge einen leeren Container für Name/Wert-Paare. Alternativ kann auch das Literal für ein Objekt {} verwendet werden. Der Name kann jede Zeichenkette sein, der Wert darf alles außer undefined sein. Auf Mitglieder kann über die Dot-Notation (Objekt.Member) oder die Index-Notation (subscript) zugegriffen werden. Die Dot-Notation ist nur zulässig, wenn der Name des Mitglieds ein gültiger Bezeichner ist. Er muss dazu mit einem Buchstaben, dem Unterstrich oder dem $-Zeichen beginnen und darf keine Leerzeichen enthalten.

Folgende Definition ist möglich:

1 var Person = {}; // dies ist erforderlich
2 Person.Vorname = "Joerg";
3 console.log(Person.Vorname);

Auch hier handelt es sich nur um eine Eigenschaft (Zeile 2):

1 var Person = {}; // dies ist erforderlich
2 Person["Meine Spezialeigenschaft"] = "Spezial Wert";
3 console.log(Person["Meine Spezialeigenschaft"])

Spezielle Typen

Speziell ist der Umgang mit nicht definierten Variablen. Prinzipiell ist keine Deklaration nötig, weil der Typ erst bei der Benutzung festgelegt wird. Deshalb wird häufig der Zustand explizit abgefragt. Ein Variable kann undefiniert sein, dass heißt dann undefined. Diese kann auch existieren, aber völlig leer sein. Das ist dann null.

null versus undefined

null ist ein Wert, welcher “Nichts” repräsentiert. Es ist ein Objekt mit dem Typ null. undefined ist dagegen kein Wert, er zeigt eine undefinierte Variable oder einen fehlenden Rückgabewert an. Es ist kein Objekt, eben gar nichts. Der Standard für nicht definierte Variablen und nicht vorhandene Member ist undefined.

4.6 Arrays

Arrays sind ebenfalls Objekte. Du kannst diese mit dem Konstruktor Array oder dem []-Literal anlegen:

Einige Methoden für Arrays

Es gibt Methoden, die ein existierendes Array verändern:

push(e)
Fügt ein Element am Ende ein und gibt die neue Länge zurück.
pop()
Entfernt das Element am Ende und gibt es zurück.
reverse()
Dreht die Reihenfolge der Elemente im Array um.
shift()
Entfernt das Element am Anfang und gibt es zurück.
sort()
Sortiert das Array und gibt das neue Array zurück.
splice(start, entfernen, neu...)
Entfernt Elemente und fügt neue ein. Die zu entfernenden werden ab start gezählt und die Anzahl mit entfernen angegeben. Alle weiteren Parameter werden als neue Elemente ab start eingefügt.
unshift(neu...)
Fügt Elemente am Anfang ein und gibt die neue Länge zurück.

Es gibt weiterhin solche, die etwas zurückgeben, jedoch am Array selbst keine Änderungen vornehmen:

join(sep)
Verbindet alle Elemente eines Arrays zu einer Zeichenkette. Als Trennzeichen – welches optional ist – wird das oder werden die Zeichen sep benutzt.
slice(start, ende)
Extrahiert den Teil eines Arrays von start bis ende.
concat
Verbindet Arrays zu einem neuen Array.
indexOf(s)
Index der ersten Fundstelle der Zeichen s oder -1, falls nichts gefunden wurde.
lastIndexOf
Index der letzten Fundstelle der Zeichen s oder -1, falls nichts gefunden wurde.

Methode, die auf Iteratoren aufsetzen, eignen sich zum Durchlaufen von Arrays:

forEach(callback, this)
Ruft eine Funktion callback für jedes Element des Arrays auf. Der Parameter this kann benutzt werden, um der Funktion den Wert für this vorzugeben.
every(callback, this)
Gibt true zurück, wenn jedes Element eines Arrays einer in der Rückruffunktion benannten Bedingung gehorcht.
some(callback, this)
Gibt true zurück, wenn mindestens ein Element eines Arrays einer in der Rückruffunktion benannten Bedingung gehorcht.
filter(callback, this)
Gibt ´die Elemente zurück, die der in der Rückruffunktion benannten Bedingung gehorchen.
map(callback, this)
Gibt die Elemente zurück, die die Rückruffunktion für jedes Element zurückgibt.
reduce(callback, init)
Reduziert ein Array, indem alle Werte gelesen werden. Der vorherige Wert wird mittels Rückruffunktion weitergereicht, sodass man addieren, subtrahieren oder sonstwas mit den Werten anstellen kann. Am Ende wird ein skalarer Wert zurückgegeben. Die Funktion beginnt links, am Index 0.
reduceRight(callback, init)
Reduziert ein Array, indem alle Werte gelesen werden. Der vorherige Wert wird mittels Rückruffunktion weitergereicht, sodass man addieren, subtrahieren oder sonstwas mit den Werten anstellen kann. Am Ende wird ein skalarer Wert zurückgegeben. Die Funktion beginnt rechts, am letzten Index.

4.7 Operatoren

Die Operatoren in JavaScript sind unspektakulär. Es ist eher der Einsatz, der einige interessant macht.

Operatoren für Objekte

Es gibt einige spezielle Operatoren, die vor allem dem Umgang mit Objekten dienen:

1 if (this instanceof Person) {
2    // Aktion, wenn passendes Objekt
3 }

Vergleichsoperatoren

Die logischen Vergleichsoperatoren sind folgende:

&& ist das logische UND, aber auch der guard-Operator. Wenn der erste Operand true ergibt, dann ist das Ergebnis der zweite Operand, sonst wird der erste Operand zurückgegeben. Das kannst du nutzen, um null-Referenzen zu vermeiden. Ohne Operator würde dafür ein if zum Einsatz kommen:

1 if (a) {
2   return a.member;
3 } else {
4   return a;
5 }

Die folgende Kurzschreibweise mit guard-Operator ist weitaus einprägsamer:

1 return a && a.member;

|| ist das logische ODER und auch der default-Operator. Wenn der erste Operand true ergibt, dann ist das Resultat der erste Operand, sonst ist das Resultat der zweite Operand. Dies kannst du verwenden, um Standardwerte zu setzen. Ohne Operator würde dafür ein if zum Einsatz kommen:

1 var meinString;
2 if (meinString == undefined || meinString.length == 0) {     
3    meinString = "mein wert"; 
4 } 

Die Kurzschreibweise mit default-Operator ist weitaus einprägsamer:

1 var meinString; 
2 meinString = meinString || "mein wert";

Das !-Zeichen ist das logische Nein (not). Wenn der Operand true ist oder ergibt, ist das Resultat false. Sonst ist das Resultat immer true. Trickreich ist !!, was du benutzen könntest, um einen Ausdruck in ein Boolean zu konvertieren:

1 var someExpression = 14;
2 var someBoolean = !!someExpression; // Ergibt: true

4.8 Anweisungen – Statements

Folgende Anweisungen stehen als Schlüsselwörter zur Verfügung:

Kontrollstrukturen: if – else

Die if-Anweisung ist eine einfache Fallunterscheidung. Der if-Ausdruck muss vom Typ Boolean sein oder er wird in ein Boolean umgewandelt. Der else-Teil kann entfallen.

1 if (Bedingung) {
2   Anweisung1;
3   Anweisung2;
4 } else {
5   Anweisung3;
6 }

Kontrollstrukturen: switch – else – default

switch dient der Fallunterscheidung. Als Ausdruck im switch und den case-Marken können Number, String und andere Typen gleichzeitig verwendet werden. Die case-Marken sollten von einem break beendet werden. Wird ein expliziter case nicht gefunden, wird der default-Block angesprungen. Der default-Block kann entfallen.

Kontrollstrukturen: Schleifen

Die Schleifen in JavaScript nutzen die Schlüsselwörter for, while und do-while.

while

Eine while-Schleife wird, wie auch die for-Schleife, so lange ausgeführt wie die Bedingung wahr (true) ist. Bitte beachte, dass die Bedingung vor dem Schleifendurchlauf getestet wird.

1 while (Bedingung) {
2   Anweisungen ;
3 }
do

Alternativ zur while-Schleife kann die do-Schleife verwendet werden, wenn die Bedingung am Ende eines jeden Schleifendurchlaufs geprüft werden soll. Bitte beachte hierbei, dass auch, wenn die Bedingung nicht zutrifft, die Schleife mindestens einmal durchlaufen wird.

1 do {
2   Anweisungen ;
3 } while (Bedingung);
for

Für Schleifen, welche im weitesten Sinne einer Aufzählung entsprechen, kommt die for-Schleife zum Einsatz, welche aus einem Anfangswert, einer Bedingung und einem Iterator besteht.

Solange die Bedingung wahr (true) ist, wird die Ausführung der Schleife fortgesetzt. Dabei ist zu beachten, dass wenn die Bedingung beim Betreten bereits falsch (false) ist, die Anweisungen innerhalb der Schleife nicht durchlaufen werden. Nach jedem Schleifendurchlauf wird die Iterationsanweisung ausgeführt und die Bedingung erneut geprüft.

Allgemein sieht das folgendermaßen aus:

1 for (Anfangswert; Bedingung; Interationsanweisung) {
2   Anweisunge(n);
3 }

Ein praktisches Beispiel zeigt das folgende Listing.

Listing: Berechnung der Quadratzahlen von 1 bis 10
1 for (i = 1; i < 11; i++) {
2   document.write("I = " + i + " I*I = " + (i*i) + "<br />");
3 }

Zunächst wird die Variable i deklariert und mit dem Wert 1 initialisiert. Bitte beachte, dass hier die Verwendung des Schlüsselwortes var nicht erforderlich ist (Scope ist immer lokal).

In der Bedingung wird überprüft, ob i kleiner 11 ist, das bedeutet, die Schleife wird 10 Mal durchlaufen. In der Iterationsanweisung wird i mit Hilfe des unären Inkrement-Operators ++ um Eins erhöht. An dieser Stelle kannst du auch i+=1 oder i=i+1 verwenden, um gegebenenfalls andere Schrittweiten vorzugeben.

Eine Variante der for-Schleife kann verwendet werden, um die Eigenschaften eines Objekts zu durchlaufen: die for in-Schleife (hier mit Ausgabe ins Dokument):

1 var ausgabe = "";
2 for (var eigenschaft in document) {
3     ausgabe = ausgabe + "document." + eigenschaft + ": " +
4        document[eigenschaft] + "<br>";
5 }
6 document.write("<h1>Eigenschaften des Objekts " +
7                "<i>document</i></h1>");
8 document.write(ausgabe);

Das Beispiel zeigt, wie du die Eigenschaften des Objekts document durchlaufen können, um beispielsweise die Fähigkeiten des jeweiligen Browsers zu ermitteln.

Abbildung: Ausgabe der Eigenschaften von document (Ausschnitt)
Abbildung: Ausgabe der Eigenschaften von document (Ausschnitt)
Schleifenkontrolle

Mitunter kommt es vor, dass eine Schleife vorzeitig abgebrochen werden soll. Hierfür wird das Schlüsselwort break verwendet. Mit Hilfe des Schlüsselwortes continue ist es möglich, die Abarbeitung mit dem nächsten Schleifendurchlauf fortzusetzen und alle folgenden Anweisungen im Anweisungsblock zu überspringen.

1 for (i = 1; i < 11; i++) {
2   if (i == 5) continue;
3   console.log("I = " + i + " I*I = " + (i * i));
4   if (i == 9) {
5     break;
6   }
7 }

Wenn i gleich 5 ist, wird die Abarbeitung der Schleife bei 6 fortgesetzt und alle weiteren Anweisungen werden übersprungen. Wenn i gleich 9 ist, wird die Schleife verlassen und keine weiteren Durchläufe ausgeführt. break und continue können sowohl für for als auch für while- und do-Schleifen verwendet werden.

Verschachtelung von Schleifen

Um bei geschachtelten Schleifen genauer festlegen zu können, welche der Schleifen mit einem continue fortgesetzt wird, wird auf eine Marke (label) mit einem Namen und einem Doppelpunkt verwiesen.

1 outer: for (i = 0; i < 10; i++) {
2     inner: for (j = 0; j < 10; j++) {
3        if (j==3) continue inner;
4        if (j==6) continue outer;
5       document.write(i + " * " + j + " = " + (i * j) +
6                      "<br />");
7     }
8   }

Den Schleifen wurden jeweils die Label inner und outer zugeordnet, um später bei der Verwendung gezielt die innere oder die äußere Schleife ansprechen zu können. So wird für alle j gleich 3 die innere Schleife fortgesetzt und für alle j gleich 6 die äußere Schleife.

Abbildung: Ausgabe (Ausschnitt vom Anfang)
Abbildung: Ausgabe (Ausschnitt vom Anfang)

Fehlerbehandlung

Ausnahmen sind Fehlerbedingungen, welche sich nicht ohne weiteres vorhersehen lassen. Eine Ausnahme wäre beispielsweise zu erwarten, wenn versucht wird, etwas auf eine Festplatte zu schreiben, diese jedoch voll ist.

Ausnahmen (Exceptions)

Folgende Schlüsselwörter stehen zur Verfügung:

Im Falle eines Fehlers kann eine Ausnahme geworfen werden. Vereinfacht ausgedrückt wird mit dem “Werfen” einer Ausnahme der Programmfluss unterbrochen und an der Stelle fortgesetzt, wo diese Ausnahme mit einem try/catch-Block abgefangen wurde. Grundsätzlich kann jedes Objekt geworfen werden, dabei spielt es keine Rolle, ob du eine Zeichenkette oder ein selbstdefiniertes Objekt verwendest. Wichtig ist das Fangen einer Exception.

 1 function ZeroDivException (msg) {
 2 
 3   this.name = 'ZeroDivException';
 4   this.message = msg === 'string' && msg.length != 0 ? msg :
 5        'Division durch Null!';
 6 
 7   this.toString = function () { return this.name + ': ' +
 8        this.message }
 9 }
10 
11 // Funktion dividiert a durch b und gibt das Ergebnis zurück
12 function div (a, b) {
13   if (b == 0)
14       throw new ZeroDivException ();
15   return a / b;
16 }
17 
18 // Try-Catch-Block zum Abfangen von Exceptions
19 try {
20 
21   document.write ('10 / 2 = ' + div (10, 2) + '<br>');
22   document.write ('5 / 0 = ' + div (5, 0) + '<br>');
23 } catch (e) {
24   document.write ('Exception aufgetreten: ' + e);
25 }

Um eine benutzerdefinierte Ausnahme werfen zu können, ist es erforderlich, eine entsprechende Klasse zu erzeugen. In diesem Beispiel heißt die Klasse ZeroDivException. Die Objektorientierung unter JavaScript wird im Abschnitt Objektorientierung noch genauer betrachtet werden. Mit Hilfe des this-Zeigers werden Eigenschaften wie der Name name und die Nachricht msg festgelegt. Soll keine Nachricht angegeben werden, kommt eine Standardnachricht zur Anwendung. Die Methode toString wird erstellt. Immer wenn ein Objekt in eine Zeichenkette zu wandeln ist, wird diese Methode automatisch aufgerufen. In der Funktion div wird überprüft, ob der zweite Parameter für die Division null ist. Für den Fall, dass der Parameter b==0 ist, wird eine entsprechende Ausnahme erzeugt und geworfen.

Mögliche Programmteile, welche eine Ausnahme werfen könnten, werden mit einem try-Block umschlossen, welcher immer von einem catch-Block gefolgt wird. Tritt jetzt eine Ausnahme auf, wird die Abarbeitung in dem try-Block abgebrochen und in dem catch-Block fortgesetzt. Ferner wird die geworfene Ausnahme innerhalb des catch-Blocks zur weiteren Verarbeitung bereitgestellt.

Optional kann der catch-Block noch von einem finally-Block gefolgt werden. Unabhängig davon, ob eine Ausnahme aufgetreten ist oder nicht, wird der finally-Block immer als letzter Block durchlaufen.

4.9 Variablen und Scope

Globale Variablen können an jeder Stelle angelegt werden, durch Zuweisen eines Wertes. Lokale Variablen werden innerhalb einer Funktion mit Hilfe des Schlüsselwortes var angelegt. Initiale Werte kannst du sofort angeben. Mehrere Variablen können durch Komma getrennt werden.

Scope – der Sichtbarkeitsbereich

In JavaScript haben {Blöcke} keinen eigenen Scope. Nur Funktionen haben einen Scope. Variablendefinitionen innerhalb einer Funktion sind außerhalb der Funktion nicht sichtbar – sie sind im Funktions-Scope (auch als Gültigkeitsbereich bezeichnet).

Das return-Schlüsselwort

Mit return weist du das Verlassen der aktuelle Funktion an. Es kann mit oder ohne Rückgabewert verwendet werden:

1 return expression;
2 return;

Ohne Ausdruck gibt return den Wert undefined zurück. Die Ausnahme hier: Konstruktoren geben die Instanz des Objekts zurück. Dazu mehr im nächsten Abschnitt.

4.10 Erstellen von Objekten

JavaScript verfügt über eine sehr einfache Unterstützung für objektorientierte Programmierung. Es wird nicht explizit von Klassen gesprochen, da sich die abstrakte Definition eines Objekts nur sehr gering von der konkreten Implementierung einer Funktion unterscheidet. JavaScript-Objekte verfügen über Eigenschaften (Attribute) und Funktionen (Methoden).

Im Wesentlichen gibt es drei Möglichkeiten ein Objekt in JavaScript zu erstellen. Im Folgenden sollen diese kurz vorgestellt werden.

Deklaration mit new

Die einfachste Methode Objekte zu erstellen, ist die Verwendung des Operators new object(). Das Prinzip ist eine Referenze an die Sprache Java, tatsächlich verhält sich new etwas anders in JavaScript.

Objekte nutzen
1 person = new Object()
2 person.name = "Jörg Krause"
3 person.height = "180cm"
4 
5 person.run = function() {
6      this.state = "running"
7      this.speed = "4ms^-1"
8 }

Ein benutzerdefiniertes Objekt person wird erstellt. Dem Objekt werden zwei Eigenschaften zugewiesen (name und height). Ferner wird eine Mitgliedsfunktion run erstellt, welche wiederum zwei neue Eigenschaften dem Objekt hinzufügt. Statt der Schreibweise new Object() kann auch das Objektliteral {} benutzt werden. Wegen seiner Kürze wird es meist bevorzugt.

Deklaration mit der Literalnotation

Die Literalnotation ist eine implizite Schreibweise, die auch als JSON (JavaScript Object Notation) bezeichnet wird.

 1 myObject = {
 2   property1: "Hello",
 3   property2: "World",
 4   property3: ["start", 1, 2, 3, "ende"],
 5 
 6   method1: function(){
 7               console.log("Methode wurde aufgerufen: " + 
 8                            this.property1);
 9             }
10 };
11 myObject.method1();

Zunächst wird myObject angelegt und die Eigenschaften property1 bis property3 werden mit Werten belegt (Zeile 2 bis 4). Dann wird eine Memberfunktion method1 (Zeile 6) deklariert. Folgende Ausgabe ergibt sich hier:

Ausgabe
Ausgabe

Die Konstruktorfunktion

Die bisher gezeigten Varianten ein Objekt anzulegen sind etwas eingeschränkt, da keine Möglichkeit besteht, mehrere Instanzen des gleichen Objekts anzulegen. Ferner ist es auch nicht möglich, beim Anlegen Parameter für die Initialisierung des Objekts mitzugeben. Aus diesem Grund soll an diese Stelle die dritte Variante für das Erstellen eines Objekts mittels Prototyping gezeigt werden. Zunächst wird ein abstraktes Objekt deklariert und anschließend wird dieses mit einem konkreten Parameter instanziiert.

 1 function Cat(name) {
 2    this.name = name;
 3    this.talk = function() {
 4       alert( this.name + " macht miau!" )
 5    }
 6 }
 7 
 8 // Verwendung
 9 cat1 = new Cat("Juri");
10 cat1.talk(); //zeigt "Juri macht miau!"
11 
12 cat2 = new Cat("Wanja");
13 cat2.talk(); //zeigt "Wanja macht miau!"

Die abstrakte Deklaration des Konstruktors des Objekts unterscheidet sich zunächst nicht von der Deklaration einer Funktion. Der Unterschied liegt in der Benutzung von new. Folgender Ablauf passiert intern:

Wird nun das Schlüsselwort this zusammen mit einer Eigenschaft oder einer Methode verwendet, wird auf das neue Objekt verwiesen, nicht auf die Deklaration. Praktisch hat man so eine Unterscheidung zwischen der Deklaration (lies: Klasse) und der Instanz.

Nachdem der Prototyp des Objekts angepasst wurde, kann wieder eine Instanz unter Verwendung des Schlüsselwortes new erstellt werden. Die Instanz wird verwendet, indem die Eigenschaft oder Methode mit einem Punkt “.” getrennt aufgerufen wird. Alternativ ist auch hier die Schreibweise mit der Index-Notation möglich: cat["talk"]().

Besonders hervorzuheben ist die Funktion, das bereits existierende Objekt von den per Prototyp hinzugefügten Funktionen und Eigenschaften profitieren. Dies ist aber nur dann der Fall, wenn bei der Deklaration kein Definition erfolgte. Im folgenden Beispiel “lernen” die Instanzen die Funktion talk erst später:

 1 function Cat(name) {
 2    this.name = name;
 3 }
 4 
 5 // Instanzen erstellen
 6 cat1 = new Cat("Juri");
 7 cat2 = new Cat("Wanja");
 8 
 9 Cat.prototype.talk = function() {
10    alert( this.name + " macht miau!" );
11 };
12 
13 cat1.talk(); //zeigt "Juri macht miau!"
14 cat2.talk(); //zeigt "Wanja macht miau!"

Anders sieht es aus, wenn es die Funktion bereits gibt:

 1 function Cat(name) {
 2    this.name = name;
 3    this.talk = function () { alert('stumm'); };
 4 }
 5 
 6 // Instanzen erstellen
 7 cat1 = new Cat("Juri");
 8 cat2 = new Cat("Wanja");
 9 
10 Cat.prototype.talk = function() {
11    alert( this.name + " macht miau!" );
12 };
13 
14 cat1.talk(); //zeigt "stumm"
15 cat2.talk(); //zeigt "stumm"

Der Prototyp wird zwar geändert, aber die Instanz-Funktion ist davor erstellt und deshalb im Suchbaum zuerst dran. Der Interpreter findet diese Stelle erst und führt sie aus. Der Prototyp wird nicht mehr erreicht. Das ist das normale Verhalten, denn der Prototyp dient lediglich dazu, eine Standarddefinition bereitzustellen, für den Fall, dass es keine explizite Definition gibt. Gibt es diese aber, ist es nicht mehr notwendig, im Prototyp nachzuschauen. Die folgende Grafik zeigt den Suchbaum.

Object.prototype.talk | +----Cat.prototype.talk ('macht miau') | +----cat1.talk ('stumm')

Die Ausgabe von ‘stumm’ ist im finalen Objekt und der Suchbaum kommt nicht mehr zur Anwendung.

Methoden und Eigenschaften

Das Hinzufügen von Methoden und Eigenschaften zu Laufzeit ist der primäre Zweck der Eigenschaft prototype. Du bekommst nachträglich (genau genommen jederzeit) Zugriff auf die Definition des Objekts. Auf diese Weise kann das vorletzte Beispiel schnell um eine Funktion zum Umbenennen erweitert werden.

1 Cat.prototype.changeName = function(name) {
2       this.name = name;
3 }
4 
5 firstCat = new Cat("Juri")
6 firstCat.changeName("Wanja")
7 firstCat.talk() // zeigt "Wanja sagt miau!"

Der Eigenschaft prototype des Objekts Cat wird eine Funktion changeName (Zeile 1) hinzugefügt, indem diese einfach zugewiesen wird. Auf diese Weise wäre es denkbar, einem Browser, welcher Objekte enthält, denen Funktionen oder Eigenschaften fehlen, diese quasi nachzurüsten.

Umgang mit Eigenschaften

Neuere Implementierung mit ECMAScript 5.1-Unterstützung erlauben die Deklaration von getrennten Get- und Set-Methoden für Eigenschaften. Dss folgende Beispiel zeigt eine solche Deklaration:

 1 var Rectangle = function (width, height) { 
 2   this._width = width; 
 3   this._height = height; 
 4 }; 
 5 
 6 Rectangle.prototype = { 
 7   set width (width) { 
 8      this._width = width; 
 9   }, 
10   get width () { 
11     return this._width; 
12   }, 
13   set height (height) { 
14     this._height = height; 
15   }, 
16   get height () { 
17     return this._height; 
18   }, 
19   get area () { return this._width * this._height; } 
20 }; 
21 var r = new Rectangle(50, 20); 

Erfolgt nun der Aufruf der Eigenschaft area, so wird 1000 ausgegeben. Tatsächlich handelt es sich hier – wie bei vielen anderen Programmiersprachen auch – um Eigenschaftsmethoden. Die Funktionsklammern () werden weggelassen, wenn die Schlüsselwörter set beziehungsweise get benutzt worden sind.

Private Variablen und Funktionen

In JavaScript sind alle Mitglieder öffentlich. Jede Funktion kann auf diese Mitglieder zugreifen, weitere hinzufügen, diese verändern oder entfernen. Jedoch gibt es die Möglichkeit, private Mitglieder zu erzeugen, auf die nur von privilegierten Funktionen zugegriffen werden kann. Dazu wird die Eigenschaft von JavaScript benutzt, dass Variablen in Funktionen nur in diesem Gültigkeitsbereich sichtbar sind.

Private Mitglieder werden innerhalb des Konstruktors deklariert. Eigenschaften werden mit Hilfe des Schlüsselwortes var angelegt. Funktionen werden innerhalb des Konstruktors definiert.

 1 function SomeObject(param) {
 2 
 3     function dec() {
 4         if (secret > 0) {
 5             secret -= 1;
 6             return true;
 7         } else {
 8             return false;
 9         }
10     }
11 
12     this.member = param;
13     var secret = 3;
14     var that = this;
15 }

Der Konstruktor für das Objekt SomeObject wird definiert (Zeile 1). Innerhalb des Konstruktors werden drei Variablen für die Instanzen des Objekts festgelegt. Der Parameter param wird einer öffentlichen Variablen member (Zeile 11) zugewiesen. Ferner werden zwei private Eigenschaften secret (Zeile 12) und that (Zeile 13) definiert.

Die private Funktion dec (Zeile 3) kann auf die privaten Eigenschaften zugreifen, weil diese innerhalb der gleichen Funktion (dem Konstruktor) deklariert sind. Jedoch können private Funktionen nicht auf die Instanzvariable this und damit nicht auf die anderen Mitglieder zugreifen. Aus diesem Grund wurde eine zusätzliche private Variable that eingeführt, die eine Referenz auf this enthält. So können auch private Funktionen auf öffentliche Mitglieder zugreifen.

Öffentliche (public) Funktionen können nicht direkt auf private Funktionen und Variablen zugreifen. Hierfür sind sogenannte privilegierte Funktionen erforderlich.

Privilegierte Funktionen

Eine privilegierte Funktion kann auf private Eigenschaften und Funktionen zugreifen, ist jedoch selbst öffentlich zugreifbar. Alle Funktionen, welche innerhalb des Konstruktors mit Hilfe des this-Operators zugewiesen werden, sind privilegierte Funktionen.

 1 function SomeObject(param) {
 2 
 3   function dec() {
 4       if (secret > 0) {
 5           secret -= 1;
 6           return true;
 7       } else {
 8           return false;
 9       }
10   }
11 
12   this.member = param;
13   var secret = 3;
14   var that = this;
15 
16   this.privileged = function () {
17       if (dec()) {
18           return that.member;
19       } else {
20           return null;
21       }
22   };
23 }

Die privilegierte Funktion (Zeile 16) kann sowohl auf die privaten Funktionen (Zeile 12) und Eigenschaften als auch auf öffentliche Mitglieder (Zeile 17) zugreifen.

4.11 Vertiefung des Prototyping

Prototyping ist in JavaScript die Technik, die beim Umgang mit Objekten die fehlenden Vererbungsfunktionen ersetzt. Zuvor jedoch eine kurze Wiederholung der Eigenschaften von Objekten.

Objekte sind Dictionaries (Maps) und können sehr flexibel erstellt werden. Die interne Umsetzung erlaubt alternativ zur intuitiven Objektschreibweise auch die Nutzung einer Art Array-Syntax. Hier ein einfaches Objekt:

1 var o1 = {};
2 o1.x = 3;
3 o1.y = 4;

Alternativ kann die Zuweisung der Eigenschaften auch sofort erfolgen:

1 var o2 = { 
2   x : 3 
3 };
4 
5 var o3 = {
6   first: o2,
7   second: function() { return 5; }
8 }

Das letzte Beispiel definiert second() als Methode. Alternativ zum Punkt kann auch folgende Schreibweise benutzt werden:

1 o2.x === 3;
2 o2["x"] === 3;
3 o2["the answer!"] = 42;

Der Vorteil der Array-Schreibweise liegt in der Möglichkeit, das Argument durch eine Variable zu ersetzen und Eigenschaften und Methoden so dynamisch nutzen und erzeugen zu können.

Objekte stammen von Object ab

Betrachte ein weiteres Objekt mit einer Methode:

1 var o1 = { };
2 o1.valueOf() // Ausgabe: "[object Object]"

Dies funktioniert, obwohl hier vorab die Methode valueOf() nicht definiert wurde. Das liegt daran, dass jedes Objekt von der internen Klasse Object erbt und diese bringt die Methode valueOf() mit – valueOf() ist im Prototyp der Klasse Objectdefiniert.

Prototypisch geerbte Methoden können überdeckt werden:

1 var o2 = {
2   valueOf: function() {
3     return 10;
4   }
5 };
6 o2.valueOf() // 10

Das Überdecken führt dazu, dass nun die eigene Definition ausgeführt wird.

Auswahl Basismethoden

Folgende eingebaute Methoden stehen für alle Objekte zur Verfügung:

Eingebaute Eigenschaften sind folgende:

Objektvererbung

Schaue zuerst auf eine Objektdefinition. Von dieser wird sodann eine Instanz angelegt, die unmittelbar erbt:

1 var point2d = { x:10, y:20 };
2 var point3d = Object.create(point2d);
3 point3d.z = 30;

Das Anlegen von Instanzen erfolgt mit Object.create. Der Prototyp des Objekts, der die zu erbendenen Elemente enthält, wird dabei quasi nebenbei erzeugt. Dies erlaubt freilich nicht die Nutzung einer Konstruktor-Funktion, die das Objekt mit Stammeigenschaften versehen könnte. Das Objekt point3d enthält deshalb nicht, wie vielleicht erwartet, die Eigenschaften x und y. Um dies zu erreichen, kann folgendes Pattern helfen:

1 function create(parent) {
2   var F = function() {};
3   F.prototype = parent;
4   return new F();
5 }

Mit einer Konstruktorfunktion sieht das dagegen folgendermaßen aus:

1 function SomeConstructor() {
2 }
3 
4 var o = new SomeConstructor();

Hier wird als “Erbmasse” der Inhalt der Eigenschaft prototype der Funktion SomeConstructor benutzt. Die ist vorhanden, weil function selbst ein Objekt ist und dieses Verhalten von Object geerbt hat. Das Schlüsselwort new greift also auf SomeConstructor.prototype zu.

Umgang mit prototype und proto

Die Eigenschaft prototype ist nur für Funktionen definiert:

Function.prototype

Dies ist eine Funktion zum Anlegen von Prototyp-Membern, also vererbbaren Elementen. __proto__ dagegen ist der interne via prototype geerbte Bestand an Membern. __proto__ ist daher immer verfügbar, egal ob Funktion oder nicht.

Die Objektvererbung mittels __proto__-Prototype

Schaue dir folgendes Beispiel genau an:

 1 var a = {
 2   x: 10,
 3   calculate: function (z) {
 4     return this.x + this.y + z;
 5   }
 6 };
 7 
 8 var b = {
 9   y: 20,
10   __proto__: a
11 };
12 
13 var c = {
14   y: 30,
15   __proto__: a
16 };
17 
18 // Aufruf der geerbten Funktionen
19 b.calculate(30); // 60
20 c.calculate(40); // 80

Das interne Mitglied __proto__ liefert die final ererbten Elemente. Wird es überschrieben, ergibt der zugewiesene Inhalt den finalen Bestand an Mitgliedern.

4.12 Exkurs Objekthierarchie

In diesem Abschnitt sollen einige Aspekte der objektorientierten Programmierung anhand eingebauter Funktionen vertieft werden.

Umgang mit this

Die Funktion call, die jedes Funktionsobjekt kennt, ruft die Funktion auf und übergibt ein Objekt als this in den Sichtbereich dieser Funktion. Optional werden Argumente übergeben.

function.call(obj, arg, arg, arg)

Die Funktion apply funktioniert wie call, aber die Argumente args sind ein Objekt vom Typ Array.

function.apply(obj, args)

 1 var obj = {
 2   sum: function(x,y) { return x + y; }
 3 }
 4 obj.sum(3,4);
 5 
 6 var obj2 = {
 7   offset: 7,
 8   sum: function(x, y) {
 9 	return x + y + this.offset;
10   }
11 }
12 
13 obj2.sum(3,4);
14 
15 var f = obj2.sum;
16 f(3,4) // NaN;
17 
18 f.call(obj2, 3, 4);

Im Beispiel manipuliert der Aufruf in Zeile 18 das this in Zeile 9. Der Aufruf in Zeile 16 gibt NaN (not a number) zurück, weil this bei der Benutzung von Referenzen – dies passiert durch die Zuweisung in Zeile 15, auf undefined gesetzt wird.

Des weiteren kommt die Methode bind zum Einsatz, die das Referenzobjekt this einmalig fest bindet und künftige Manipulationen unterdrückt.

 1 var Button = function(content) { 
 2   this.content = content;
 3 };
 4 Button.prototype.click = function() {
 5   console.log(this.content);
 6 }
 7 
 8 var myButton = new Button('OK');
 9 myButton.click();
10 
11 var looseClick = myButton.click;
12 looseClick(); 
13 
14 var boundClick = myButton.click.bind(myButton);
15 boundClick();

Die Ausgabe ist: “OK”, “undefinded”, “OK”. Die Ausgabe “undefined” kommt zustande, weil beim Aufruf von looseClick keine Referenz auf this vorhanden ist. Bei boundClick dagegen wurde diese zuvor mit bind festgelegt. Der nächste Abschnitt “Vertiefung zu Bindungen” geht weiter darauf ein.

4.13 Vertiefung zu Bindungen

Als Bindung ist in Programmiersprachen der Zeitpunkt der Bereitstellung von Variablen in Abhängigkeit vom Programmfluss gemeint. Normalerweise must du dich als Entwickler selten um die Bindung kümmern. Dies erledigt der Compiler oder Interpreter. Die Reihenfolge der Definition spielt keine Rolle. Darüberhinaus ist es üblich, dass Zugriffe ohne explizite Angabe des Sichtbereiches immer das aktuelle Objekt adressieren. Die explizite Angabe einer Zugriffsform mit einem Schlüsselwort wie this ist meist möglich, aber nicht notwendig. Wenn du von einer anderen Programmiersprache auf JavaScript wechselst, solltest du unbedingt beachten, das JavaScript (neben PHP übrigens), die große Ausnahme von dieser Regel ist. JavaScript erfordert eine explizite Angabe des Sichtbereiches, auch wenn es sich lediglich um die aktuelle Methode handelt.

Das liegt im Wesentlichen daran, dass JavaScript im traditionellen Sinne nicht objektorientiert ist – oder zumindest keine rein objektorientierte Sprache. Die prototypische Vererbung, die zuvor bereits vorgestellt wurde, ist eine andere Art des Umgangs mit Objekten. Damit einher geht ein anderes Bindungsverhalten – die Bindung ist immer explizit. Die explizite Bindung bezieht mit ein, dass das aktuelle Objekt nicht das ist, was man als Entwickler intuitiv erwartet. Dies wurde bereits bei der Vorstellung der Methode call gezeigt.

Das folgende Beispiel zeigt, was passiert, wenn dieses Verhalten nicht beachtet wird.

1 var john = {
2   name: 'Joerg',
3   greet: function(person) {
4     alert("Hallo " + person + ", ich bin " + name);
5   }
6 };
7 john.greet("Clemens");
8 // Ergibt: "Hallo Joerg, ich bin "

Da ist sicher nicht das erwartete Verhalten. Der Name erscheint nicht wie gedacht. Das Problem ist der Zugriff auf die Objekteigenschaft name in Zeile 4. Hier fehlt die Bindungsreferenz. Die Annahme, es würde schon das aktuelle Objekt sein (Joerg aus Zeile 1), ist schlicht falsch. Hier geht JavaScript einfach auf Suche nach dem Namen oberhalb der Definition. Das Ende ist das Objekt window, das keine Eigenschaft name hat und deshalb ist der Name leer (der Fehler wirkt sich nicht drastischer aus, weil der Rückgabewert undefined hier in eine leere Zeichenkette konvertiert wird).

Nun ein weiterer Versuch:

1 name = 'Uwe'; // Entspricht: window.name = 'Clemens';
2 var john = {
3   name: 'Joerg',
4   greet: function(person) {
5     alert("Hallo " + person + ", ich bin " + name);
6   }
7 };
8 john.greet("Clemens");
9 // => "Hallo Clemens, ich bin Uwe"

Das funktioniert nur scheinbar richtig. Denn nun wurde tatsächlich dynamisch eine neue Eigenschaft name dem window-Objekt hinzugefügt. Globale Namen auf dieser Ebene sind aber ein Antipattern – vermeiden das, wann immer es geht.

Korrekt ist die Benutzung von this:

1 var jk = {
2   name: 'Joerg',
3   greet: function(person) {
4     alert("Hallo " + person + ", ich bin " + this.name);
5   }
6 };
7 jk.greet("Clemens");
8 // => "Hallo Clemens, ich bin Joerg"

Das Schlüsselwort this ist hier die explizite Bindung an einen bestimmten Kontext – nämlich den des Aufrufers – was in diesem Fall das aktuelle Objekt ist.

Ausgabe
Ausgabe

Ein weiteres Beispiel zeigt, wie schnell auch this tückisch werden kann. Typisch sind Referenzen auf Funktionen, wie hier in Zeile 7:

1 var jk = {
2   name: 'Joerg',
3   greet: function(person) {
4     alert("Hallo " + person + ", ich bin " + this.name);
5   }
6 };
7 var fx = jk.greet;
8 fx("Clemens");
9 // Ergibt: "Hallo Clemens, ich bin "

Trotz der Angabe der expliziten Bindung funktioniert das nicht wie erwartet. this hat hier offensichtlich eine andere Bedeutung bekommen. Bei Referenzen geht die Bindung verloren, this weiß nicht, wo es herkommt. Hier hilft nur, die Bindung beim Aufruf explizit zu steuern. Ansonsten sucht JavaScript wieder den Objektbaum aufwärts und endet erneut bei window (im Browser) oder global (in Node).

Code, der Bindungen berücksichtigt

Code sollte die Bindungen explizit berücksichtigen. Wenn eine Methode als Argument einer anderen Methode übergeben wird, dann gelangt die ausführende Instanz in den Kontext des Aufrufers und kann dann lokal binden. Hier ein Beispiel, zuerst mit einer Klassendefinition:

 1 function Person(first, last, age) {
 2   this.first = first;
 3   this.last = last;
 4   this.age = age;
 5 }
 6 Person.prototype = {
 7   getFullName: function() {
 8     alert(this.first + ' ' + this.last);
 9   },
10   greet: function(other) {
11     alert("Hallo " + other.first + ", ich bin " +
12     this.first + ".");
13   }
14 };

Nun wird dieser Code benutzt:

1 var jk = new Person('Joerg', 'Krause', 50);
2 var ck = new Person('Clemens', 'Krause', 25);
3 ck.greet(jk);

Das funktioniert soweit gut. Nun eine Erweiterung:

1 function times(n, fx, arg) {
2   for (var index = 0; index < n; ++index) {
3     fx(arg);
4   }
5 }
6 times(3, ck.greet, jk); //
7 
8 times(1, jk.getFullName);
Erste Ausgabe (in Chrome via JSFiddle)
Erste Ausgabe (in Chrome via JSFiddle)
Zweite Ausgabe (in Chrome via JSFiddle), erscheint 3 mal
Zweite Ausgabe (in Chrome via JSFiddle), erscheint 3 mal
Dritte Ausgabe (in Chrome via JSFiddle), erscheint 1 mal
Dritte Ausgabe (in Chrome via JSFiddle), erscheint 1 mal

Hier ging die Bindung wieder verloren – JavaScript liest undefined. Nun kann das Problem leicht gelöst werden. Wenn der Aufruf jedoch in ein Framework geht und die Bindung nicht sichtbar ist, kann es sehr schwer sein, die Ursache für die Fehlfunktion zu finden. Hier ein Beispiel:

1 this.items.each(function(item) {
2   // Process item
3   this.markItemAsProcessed(item);
4 });

Dies funktioniert nicht, weil markItemAsProcessed über this nicht gefunden wird. Die Methode each stammt aus einer Bibliothek, auf die per Referenz verwiesen wird. Diese Referenz “bricht” den Zugriff (die Bindung) auf das aktuelle Objekt, indem die Methode markItemAsProcessed definiert wurde. Der Aufrufer bestimmt hier, was this tatsächlich ist. Das kann funktionieren, muss aber nicht. Meist wird this in der Tat manipuliert, um einen bequemen Zugriff auf einen Kontext oder eine Ereignisquelle zu erhalten.

Explizite Bindungen

Das explizite Binden erfolgt über die Methoden apply, call und bind. Jedes Funktionsobjekt (lies: Funktion), verfügt über diese Methode – sie werden vom Funktionsprototypen geliefert.

1 var fx = ck.greet;
2 fx.apply(ck, [jk]);

Der Aufruf (Zeile 2) erfolgt hier über apply. Der Kontext für die Bindung ist das erste Argument. Danach folgt ein Array mit allen anderen Argumenten der indirekt aufgerufenen Methode – egal wie viele das sind.

Die Methode call unterscheidet sich nur insofern, als dass hier die Argumente explizit angegeben werden und passen müssen.

1 var fx = ck.greet;
2 fx.call(ck, jk);

Besonders tückisch ist die Tatsache, dass nach dem Verlust der Bindung die explizite Bindung jedes Objekt akzeptiert. Es muss überhaupt kein Zusammenhang mit dem aktuellen Code bestehen. Das kann zu komplett unwartbarem und vor allem auch unlesbarem Code führen. Disziplin ist hier unerlässlich.

Solche freien Bindungen sind selten sinnvoll. Besser wäre es, man könnte die Bindung beschränken. Das geht nur über das Verschachteln von Funktionen, bei denen das durchgereichte Kontextobjekt unter Kontrolle ist:

1 function createBoundedWrapper(object, method) {
2   return function() {
3     return method.apply(object, arguments);
4   };
5 }

Und so wird das benutzt:

1 var ckGreet = createBoundedWrapper(ck,
2                                       ck.greet);
3 ckGreet(jk);

Das funktioniert, die Referenz ist festgelegt. Sie kann von außen auch nicht gestört werden.

Zusammenfassung

Ein paar allgemeine Regeln zum Umgang mit objektorientierten Techniken sollen dazu dienen, JavaScript hier besser einordnen zu können:

4.14 Module

JavaScript ist eine Sprache, die nicht durch besonders viele Schlüsselwörter glänzt. Die Einfachheit hat seinen Preis, wenn umfangreichere Applikationen entstehen. Damit trotz der Beschränkungen eine hohe Code-Qualität möglich wird, sind viele Entwurfsmuster (pattern) entstanden, die die fehlenden sprachlichen Ausdrucksmöglichkeiten ersetzen.

Das Modul-Entwurfsmuster ist eines der am häufigsten eingesetzten. Mangels Namensräumen ist die Isolation von Code von herausragender Bedeutung. Mit dem Modul-Entwurfsmuster wird genau das erreicht. Darüberhinaus kann hier auch eine Aufteilung des Modulcodes auf verschiedene Dateien erfolgen. Dies erleichtert die Übersichtlichkeit und Wartbarkeit durch Modularisierung – daher der Name.

Zum Vergleich soll hier ein Blick auf ein klassisches unstrukturiertes Skript erfolgen. Zahlreiche Scripte, die du im Netz finden kannst, liegen in einer gesonderten Datei vor und sind darüber hinaus unstrukturiert. Es handelt sich um eine lose Sammlung von globalen Variablen und Funktionen:

 1 var cnt = 0;
 2 function increment() {
 3   return cnt++;
 4 }
 5 function reset() {
 6   cnt = 0;
 7 }
 8 function show() {
 9   console.log(cnt);
10 }
11 reset();
12 increment();
13 increment();
14 show();

Diese Organisation führt dazu, dass das Skript nicht gut konfigurierbar, anpassbar und erweiterbar ist. Am schwersten wiegt jedoch, dass es sich um eine große Zahl von Objekten im globalen Sichtbereich handelt. Globale Variablen und Funktionen sind Eigenschaften des window-Objektes, wenn JavaScript im Browser läuft, bzw. global in Node. Ein schwaches Hilfsmittel besteht oft darin, benutzerspezifische Präfixe zu benutzen:

 1 var jk_cnt = 0;
 2 function jk_increment() {
 3   return jk_cnt++;
 4 }
 5 function jk_reset() {
 6   jk_cnt = 0;
 7 }
 8 function jk_show() {
 9   console.log(jk_cnt);
10 }
11 jk_reset();
12 jk_increment();
13 jk_increment();
14 jk_show();

Unstrukturierte Skripte sind schlecht zu warten und kollidieren mit anderen Skripten. Die Namensgebung ist in der Regel ein schwacher und unsicherer Schutz. Es ist naheliegend, das globale Variablen – sorgfältig benannt oder nicht – vermieden werden sollten. Insbesondere ist dies deshalb beachtenswert, weil der globale Adressraum im Browser auch das Objektmodell der HTML-Seite enthält und Konflikte sich unmittelbar schädlich auf das Browserverhalten und die Anzeige auswirken können.

Der Kapselung der eigenen Codes kommt also große Bedeutung zu. Darum geht es im Modul-Entwurfsmuster. Datenkapselung bedeutet, dass das Erweitern des globalen Objekts sowie der DOM-Objekte auf ein Minimum reduziert wird. Du solltest nur dann Objekte im window-Objekt speichern, wenn der Zugriff von außen unbedingt notwendig sind.

Die öffentliche API Ihres Skriptes benötigt nur ein globales Objekt, über welches die restlichen Funktionen zugänglich sind. Bei manchen Aufgaben ist es möglich, ein Skript konsequent zu kapseln, sodass es das globale window-Objekt überhaupt nicht antastet. In anderen Fällen ist es nötig, zumindest ein Objekt global verfügbar zu machen. Große Bibliotheken dienen hier oft als Vorbild. Das große jQuery-Framework definiert standardmäßig nur zwei globale Variablen: window.jQuery() und als Alias window.$(). Das YUI-Framework definiert lediglich window.YUI().

Einfache Module

Ausgangspunkt für ein Modul ist ein Objekt, dass meist durch das Objekt-Literal erstellt wird. Alle Variablen und Funktionen eines Skripts werden in einer solchen Objektstruktur untergebracht. Im globalen Geltungsbereich taucht dann nur noch diese eine Objektstruktur auf, andere globale Variablen oder Funktionen werden nicht belegt. Das Skript ist in der Objektstruktur in sich abgeschlossen. Damit sind Wechselwirkungen mit anderen Skripten ausgeschlossen, solange der Bezeichner der Objektstruktur eindeutig ist.

Ein JavaScript-Objekt ist also nichts anderes als ein Container für weitere Daten. Ein Objekt ist eine Liste, in der unter einem Bezeichner Unterobjekte gespeichert sind.

 1 var jk_obj = { };
 2 jk_obj.cnt = 0; 
 3 jk_obj.increment = function () {
 4   return this.cnt++;
 5 };
 6 jk_obj.reset = function () {
 7   this.cnt = 0;
 8 };
 9 jk_obj.show = function () {
10   console.log(this.cnt);
11 };
12 jk_obj.increment();
13 jk_obj.increment();
14 jk_obj.show();
15 jk_obj.cnt = -100; // fatal

Hier ist nun nur noch das Objekt jk_obj im globalen Gültigkeitsbereich. Das ist schon sehr gut, allerdings zeigt der Code auf Zeile 14, das es keinen Schutz der intern benutzten Eigenschaften gibt. Eine ungewollte Manipulation kan fatale Folgen haben.

Privater Funktions-Scope

Eine Kapselung erreichst du mit einer Funktion, die Ihre Variablen einschließt und nur wenige Objekte nach außen verfügbar macht.

Beim Objekt-Literal wird ein globales Objekt als Namensraum benutzt, um darin eigene Objekte unterzubringen. Diese Objekte sind über das Containerobjekt für andere Skripte zugänglich. Es gibt also keine Trennung zwischen öffentlichen und privaten Daten. Während es sinnvoll ist, dass z.B. eine Methode Modul.methode() von außen aufrufbar ist, ist es unnötig und potenziell problematisch, dass jede Objekteigenschaft gelesen und manipuliert werden kann.

Der nächste Schritt ist daher, eine wirksame Kapselung zu implementieren. Das Mittel dazu ist ein eigener, privater Variablen-Gültigkeitsbereich. Darin können beliebig viele lokale Variablen und Methoden definiert werden. Die einzige Möglichkeit, in JavaScript einen solchen Gültigkeitsbereich zu erzeugen, ist eine Funktion. Du definierst also zuerst eine Funktion, um darin das gesamte Skript zu kapseln. Solange durchgehend lokale Variablen und Funktionen verwendet werden, wird der globale Gültigkeitsbereich nicht angetastet.

Schließe deinen Code in einen Funktionsausdruck ein, der sofort ausgeführt wird. Folgendes Fragment zeigt, wie dies aussieht:

1 (function () {
2     /* ... */
3 }());

Hier wird eine namenlose Funktion per Funktionsausdruck erzeugt (Zeile 1). Diese wird mit Klammern umschlossen. Auf diesen Ausdruck wird der Call-Operator ausgeführt (Zeile 3) – die runden Klammern am Ende (). Die Anweisung wird mit einem Semikolon abgeschlossen.

Diese anonyme Funktion wird nur notiert, um einen Gültigkeitsbereich zu erzeugen, und sie wird sofort ausgeführt, ohne dass sie irgendwo gespeichert wird. Innerhalb der Funktion wird nun der gewünschte Code untergebracht:

 1 (function() {   
 2   var cnt = 0; 
 3   function increment() {
 4     return cnt++;
 5   }
 6   function reset() {  
 7     cnt = 0;
 8   }
 9   function show() {
10     console.log(cnt);
11   }
12   reset();
13   increment();
14   increment();
15   show();
16 }());

Im Beispiel finden sich Variablendeklarationen und eine Funktionsdeklarationen. Beide sind lokal, sind also nur innerhalb der Kapselfunktion zugänglich. Du kannst auf die Variablen und Funktionen intern direkt zugreifen.

Vergiss nicht, Variablen mit var als lokal zu deklarieren. Andernfalls werden sie automatisch global, also Eigenschaften von window.

Öffentliche Schnittstellen

Auch das letzte Muster ist nicht perfekt. Denn die Funktion wird genau einmal ausgeführt und dann besteht keine Zugriffsmöglichkeit mehr. Das kann ausreichend sein – meist reicht es jedoch nicht.

Das Schnittstellen-Entwurfsmuster (Revealing Module Pattern) erlaubt öffentliche und private Objekte und eignet sich ideal, um API und interne Implementierung sauber zu trennen. Diesen Kompromiss erreichst du durch eine Kombination aus Objekt-Literalen und einer Kapselfunktion.

Die grundlegende Struktur sieht folgendermaßen aus:

 1 var Modul = (function () {
 2 
 3     /* ... private Objekte ... */
 4 
 5     /* Gebe öffentliche API zurück: */
 6     return {
 7         öffentlicheMethode : function () { ... }
 8     };
 9 
10 }());

Auf das vorherige Beispiel angewendet sieht das nun folgendermaßen aus:

 1 var jk = (function() {
 2   var cnt = 0;
 3   function reset() {
 4     cnt = 0;
 5   }
 6   reset();
 7 
 8   return {
 9     increment: function () {           
10       return cnt++;
11     },        
12     show:  function () {          
13       console.log(cnt);
14     } 
15   };
16 }());
17 
18 jk.increment();
19 jk.increment();
20 jk.show();

In diesem Beispiel wird genau ein globales Objekt belegt, sodass der Zugriff wiederholt erfolgen kann. Darüberhinaus wird eine Art Schnittstelle – ein Interface – definiert, dass die Zugriffsform regelt. Dies wird durch die return-Anweisung in Zeile 8 erreicht. Hier wird ein weiteres Objekt mittels Objekt-Literal erstellt, in dem zwei Methoden erstellt werden, die ihrerseits auf private Mitglieder des Moduls zugreifen. Nach außen sind jetzt nur die Methoden show und increment sichtbar.

Instanzen von Modul-Objekten

Nun kann es passieren, dass ein Modul nicht nur eine statische Umgebung liefert, sondern mehrere Instanzen davon abstammen, die unabhängig voneinander sind. Die im Abschnitt Objektorientierung gezeigte Form der Nutzung von Funktionen als Konstruktor wird nun mit dem Modul-Entwurfsmuster kombiniert.

 1 var jk = function() {    
 2   var counterInitialValue = 0;
 3   var counterConstructor = function() { 
 4      this.cnt = counterInitialValue;     
 5   };
 6   counterConstructor.prototype.increment = function() {       
 7      this.cnt++;
 8   };
 9   counterConstructor.prototype.show = function() {      
10     console.log(this.cnt);
11   };
12   return {
13     Counter : counterConstructor   
14   };
15 }();

Erneut wird nur ein einziges Objekt im globalen Namensraum benötigt. Die Variable counterInitialValue ist intern und geschützt (Zeile 2). Auch der Konstruktor (Zeile 3) ist intern. Damit Instanzen die Methoden erben, werden diese auf dem Prototypen des Konstruktors definiert (Zeilen 6 und 9). Die Konstruktorfunktion liefert also alles, was benötigt wird. Und nur diese ist auch öffentlich (Zeile 13).

Die Benutzung dieses Moduls – wo auch immer – sieht nun folgendermaßen aus:

1 var mycnt = new jk.Counter();
2 mycnt.increment();
3 mycnt.increment();
4 mycnt.show();

Globale Objekte

Das Übergeben von Objekten in die Kapselfunktion verkürzt die Gültigkeitskette und beschleunigt den Zugriff auf diese Objekte. Ohne diese Übergabe wäre JavaScript gezwungen, immer alle erreichbaren Gültigkeitsbereich abzusuchen, ob das gewünschte Objekt nicht irgendwo erreicht werden kann. Ein Wert, der als Parameter übergeben wurde, ist in der Suchliste weit oben und wird sehr schnell gefunden.

Aus diesem Grund hat es sich eingebürgert, das window-Objekt sowie weitere häufig benutzte Objekte wie document mittels Parametern in den Funktions-Gültigkeitsbereich zu übergeben:

1 (function (window, document, undefined) {
2 
3     /* ... */
4 
5 })(window, document);

Gleichzeitig wird hier sichergestellt, dass innerhalb der Funktion der Bezeichner undefined immer den Typ undefined besitzt. Wir definieren einen solchen Parameter, aber übergeben keinen Wert dafür –- sodass eine lokale Variable namens undefined mit einem leeren Wert angelegt wird. Das ist andernfalls nicht garantiert, denn window.undefined ist durch Skripte überschreibbar.

Erweiterbare Module

Eigene Module werden genauso übergeben und können damit leicht erreicht werden. Zugleich erlaubt diese Technik das Aufteilen des Skripts auf mehrere Dateien. Module nachträglich zu erweitern ist ebenso sinnvoll und häufig benötigt, allerdings haben die einzelnen Teile keinen Zugriff auf die privaten Objekte der anderen Teilmodule.

Ein selbst definiertes Modul könnte nun folgendermaßen aussehen:

 1 var MODULE = (function () {
 2    var my = {},
 3    privateVariable = 1;
 4    function privateMethod() {
 5      // ...
 6    }
 7    my.moduleProperty = 1;
 8    my.moduleMethod = function () {
 9      // ...
10    };
11    return my;
12 }());

In einem weiteren Skript kann dieses Modul erweitert werden:

1 var MODULE = (function (my) {
2 
3   my.anotherMethod = function () {
4     // neue (weitere) Methode
5   };
6 
7   return my;
8 
9 } (MODULE));

Problematisch ist hier, dass die Reihenfolge der Definition entscheidend ist. Beim Laden der Skripte via HTTP kann die Reihenfolge aber nicht garantiert werden. Deshalb soll der default-Operator || benutzt werden, um einen beliebigen Anfangspunkt zu erlauben:

1 var MODULE = (function (my) {
2 
3   // Mehr Funktionen
4 
5   return my;
6 } (MODULE || {}));

Dieses Verfahren wird auch als “lose Augmentierung” bezeichnet. Die Funktionsweise ist einfach. Jedes Modul wird gleichartig definiert. Das zuerst ausgeführte Skript – egal welches – findet die Variable MODULE im Zustand undefined vor. Damit ist der erste Teil des Ausdrucks in Zeile 6 false. Dadurch wird der zweite Teil ausgeführt – ein leeres Objekt. Dies wird in der Kette der Modulbausteine als Parameter übergeben und dann mit Eigenschaften und Methoden sukzessive angereichert (daher der Name “Augmentierung”).

Die lose Augmentierung ist nicht immer sinnvoll, denn sie schränkt die Möglichkeiten objektorientierter Techniken etwas ein. Eine feste Augmentierung – mit definierter Reihenfolge – kann beispielsweise benutzt werden, um bereits definierte Funktionen unter bestimmten Umständen gezielt zu überschreiben oder zu verdecken.

Namensräume

Module kannst du mit einem Objekt in einem Namensraum gruppieren. Es gibt zwar keine nativen Namensräume in JavaScript, aber gekapselte, verschachtelte Objekte erfüllen denselben Zweck – ganz ohne ein weiteres Schlüsselwort.

1 var Namensraum = {};
2 Namensraum.Modul1 = (function () { ... })();
3 Namensraum.Modul2 = (function () { ... })();

Zusammenfassung

Entwurfsmuster sind essenziell in JavaScript. Die Programmierung erfordert Disziplin, ohne die schnell unwartbarer Code entsteht. Auf dem Weg zu gutem Code helfen Entwurfsmuster. Nutze diese auch bei kleinen und trivialen Projekten, um schnell Sicherheit zu gewinnen.

Die Techniken dienen vor allem dazu, die fehlenden sprachlichen Merkmale von JavaScript auszugleichen. Sie zeigen auch, das komplexe Sprachen mit vielen Schlüsselwörtern nicht zwingend erforderlich sind, um große Projekt umzusetzen.

4.15 Globale Standardfunktionen

Zu den Standardfunktionen gehören einige global verfügbare Methoden, die universell benutzt werden können. Sie sind im Browser in window definiert, in Node dageben in global, sodass man davon ausgehen kann, dass sie immer verfügbar sind.

Timerfunktionen

Folgende Methoden stehen zur Verfügung:

Nutze immer Funktionen für den Aufruf, nie Zeichenketten. Zeichenketten sind langsam und unsicher. Folgendes geht, ist aber nicht so gut:

1 setInterval('doSomethingPeriodically()', 1000);  
2 setTimeout('doSomethingAfterFiveSeconds()', 5000);

Folgendes ist besser:

1 setInterval(doSomethingPeriodically, 1000);  
2 setTimeout(doSomethingAfterFiveSeconds, 5000);

Noch einfacher ist es, die Funktion gleich anonym zu benutzen:

1 setInterval(function() {
2    // tu was jede Sekunde
3 }
4 , 1000);  
5 
6 setTimeout(function() {
7    // tu was nach 5 Sekunden
8 }, 5000);

Prüffunktionen

Tests für Zahlen sind:

1 var Zahl = Number.MAX_VALUE;
2 if (!isFinite(Zahl * 2)) {
3   alert("Die Zahl ist nicht zu verarbeiten.");
4 }

Parser für Zahlen sind:

4.16 Tipps und Tricks

In diesem Abschnitt werden einige Eigenschaften behandelt, die für die professionelle Programmierung sinnvoll sind.

Strikt-Mode use strict

Eine Funktion oder Skript kann in den Strikt-Mode versetzt werden. Ist das der Fall, wird der JavaScript-Interpreter etwas strenger:

Allgemeine Tipps

Spezielle Tipps

Dies ist eine Sammlung von Tricks für praktische Anwendungen.

Zufallswert aus einem Array holen
1 var items = [12, 548, 'Doe' , 2145 , 119];
2 
3 var  randomItem = items[Math.floor(Math.random() * items.length)];
Zufallszahl aus einem Bereich
1 var x = Math.floor(Math.random() * (max - min + 1)) + min;
Array mit fortlaufenden Zahlen
1 var numbersArray = [] , max = 100;
2 
3 for( var i=1; numbersArray.push(i++) < max;);  
4 
5 // Ausgabe: [1,2,3 ... 100] 
Set mit Zeichen erzeugen
1 function generateRandomAlphaNum(len) {
2     var rdmString = "";
3     for( ; rdmString.length < len; rdmString  += Math.random().toStr\
4 ing(36).substr(2));
5     return  rdmString.substr(0, len);
6 
7 }
Zahlenarray durcheinander bringen
1 var numbers = [5, 458 , 120 , -215 , 228 , 400 , 122205, -85411];
2 numbers = numbers.sort(function(){ return Math.random() - 0.5});
Leerzeichen entfernen (trim)
1 String.prototype.trim = function() {
2   return this.replace(/^s+|s+$/g, "");
3 };  

Neuere JavaScript-Umgebungen haben trim bereits eingebaut.

Arrays kombinieren
1 var array1 = [12 , "foo" , {name "Joe"} , -2458];
2 var array2 = ["Doe" , 555 , 100];
3 
4 Array.prototype.push.apply(array1, array2);
Argumente in Array konvertieren
1 var argArray = Array.prototype.slice.call(arguments);
Wert auf Zahl testen
1 function isNumber(n){
2     return !isNaN(parseFloat(n)) && isFinite(n);
3 }
Wert auf Array testen
1 function isArray(obj){
2     return Object.prototype.toString.call(obj) === '[object Array]' ;
3 }

Neuere Browser kennen Array.isArray(obj); und benötigen dies nicht mehr.

Minimum und Maximum in einem Array
1 var  numbers = [5, 458 , 120 , -215 , 228 , 400 , 122205, -85411]; 
2 var maxInNumbers = Math.max.apply(Math, numbers); 
3 var minInNumbers = Math.min.apply(Math, numbers);
Array leeren
1 var myArray = [12 , 222 , 1000 ];  
2 myArray.length = 0; // myArray will be equal to [].
Array-Element entfernen

Folgendes geht nicht, da delete das Element auf undefined setzt, aber nicht entfernt.

1 var items = [12, 548 ,'a' , 2 , 5478 , 'foo' , 8852, , 'Doe' ,2154 ,\
2  119 ]; 
3 items.length; // return 11 
4 delete items[3]; // return true 
5 items.length; // return 11 

Folgendes funktioniert:

1 var items = [12, 548 ,'a' , 2 , 5478 , 'foo' , 8852, , 'Doe' ,2154 ,\
2  119 ]; 
3 items.length; // return 11 
4 items.splice(3,1) ; 
5 items.length; // return 10 
Array abschneiden
1 var myArray = [12 , 222 , 1000 , 124 , 98 , 10 ];  
2 myArray.length = 4; // Ergibt: [12 , 222 , 1000 , 124]
1 myArray.length = 10; // the new array length is 10 
2 myArray[myArray.length - 1] ; // undefined
Runden
1 var num =2.443242342;
2 num = num.toFixed(4);  // num ergibt: 2.4432
Gleitkommetipps
1 0.1 + 0.2 === 0.3    // ist false 
2 9007199254740992 + 1 // ist 9007199254740992  
3 9007199254740992 + 2 // ist 9007199254740994

Warum passiert das? 0.1 +0.2 ist intern 0.30000000000000004. Das passiert, wenn rationale Zahlen in einem 64bit-Raum abgebildet werden. Mit toFixed() und toPrecision() kann dies gelöst werden.

Zugriff auf Prototyp vermeiden
1 for (var name in object) {  
2     if (object.hasOwnProperty(name)) { 
3         // Aktion  
4     }  
5 }

Dies ist vor allem sinnvoll, wenn das Basisobjekt sehr groß ist, z.B. ist dies bei HTML-Elementen der Fall.

Der Komma-Operator
1 var a = 0; 
2 var b = ( a++, 99 ); 
3 console.log(a);  // a will be equal to 1 
4 console.log(b);  // b is equal to 99
Prüfe bevor isFinite() benutzt wird
1 isFinite(0/0) ; // false 
2 isFinite("foo"); // false 
3 isFinite("10"); // true 
4 isFinite(10);   // true 
5 isFinite(undefined);  // false 
6 isFinite();   // false 
7 isFinite(null);  // true!
Vermeide negative Indizes in Arrays
1 var numbersArray = [1,2,3,4,5]; 
2 var from = numbersArray.indexOf("foo") ;  // ergibt: -1 
3 numbersArray.splice(from,2);    // ergibt: [5]
for in ist nicht gut in Arrays

Folgendes ist keine gute Idee:

1 var sum = 0;  
2 for (var i in arrayNumbers) {  
3     sum += arrayNumbers[i];  
4 }

So sollte es aussehen:

1 var sum = 0;  
2 for (var i = 0, len = arrayNumbers.length; i < len; i++) {  
3     sum += arrayNumbers[i];  
4 }

Folgendes ist auch keine so gute Idee:

1 for (var i = 0; i < arrayNumbers.length; i++)

Dabei wird die Länge bei jedem Durchlauf neu berechnet. Sie ändert sich aber möglicherweise nie. Neuere Engines berücksichtigen dies automatisch.