Eine Wiedereinführung in JavaScript

von 2 Mitwirkenden:

Einleitung

Warum eine Wiedereinführung? Weil von JavaScript nicht ganz zu unrecht behauptet wird, die am meisten missverstandene Programmiersprache der Welt zu sein. Obwohl die Sprache oft als Spielzeug bezeichnet und abgewertet wird, besitzt sie neben ihrer Einfachheit einige mächtige Features. Im Jahr 2005 erschienen verschiedene hochentwickelte JavaScript-Applikationen, welche zeigten, dass sich tiefgreifendes Fachwissen in diesem Bereich für jeden Webentwickler auszahlt.

Zunächst wollen wir uns ein wenig mit der Entwicklungsgeschichte von JavaScript beschäftigen: JavaScript wurde im Jahr 1995 von Brendan Eich entworfen, der bei Netscape als Software Engineer arbeitete und 1996 mit Netscape 2 zum ersten Mal herausgegeben. Anfangs sollte die Sprache den Namen LiveScript bekommen, wurde dann jedoch aufgrund einer verhängnisvollen Entscheidung, die auf bessere Vermarktung abzielte und womit an den Erfolg von Java angeknüpft werden sollte, in JavaScript umbenannt - und das obwohl die beiden Sprache sehr wenig gemeinsam haben. Dieser Umstand sorgt bis heute immer wieder für Verwirrung.

Microsoft gab eine weitgehend kompatible Version der Sprache heraus und nannte sie JScript, welche einige Monate später mit dem Internet Explorer 3 veröffentlicht wurde. Netscape übergab die Sprache an Ecma International, eine Europäische Standardorganisation, und im Jahr 1997 erschien die erste Edition des ECMAScript-Standards. Der Standard bekam 1999 ein bedeutendes Update mit ECMAScript Edition 3 und hält sich seither sehr stabil. Die vierte Edition des Standards wurde aufgrund von poltischen Meinungsverschiedenheiten zur Komplexität der Spache fallengelassen, stellte jedoch eine Basis bei der Entwicklung der im Dezember 2009 herausgegebenen fünften Edition des Standards dar.

Für Entwickler ist es sehr erfreulich, dass sich der Standard nicht groß verändert hat, da so die einzelnen abweichenden Implementierungen die Zeit hatten, bei der Weiterentwicklung den Standard zu berücksichtigen und aufzuholen. Ich werde in diesem Tutorial fast ausschließlich den Dialekt der Edition 3 des Standards verwenden und durchweg den Begriff JavaScript benutzen.

Anders als viele andere Programmiersprachen, gibt es bei JavaScript kein Konzept für Eingabe und Ausgabe, da JavaScript als Skriptsprache für eine Hostumgebung entworfen wurde und es die Aufgabe dieser Umgebung ist, Mechanismen für die Kommuniktion mit der Außenwelt bereitzustellen. Die hierfür am meisten genutzte Hostumgebung ist der Browser, doch findet man Interpreter für JavaScript auch in Adobe Acrobat, Photoshop, der Widget Engine von Yahoo! und serverseitigen Applikationen wie node.js. Des Weiteren wird JavaScript auch bei NoSQL-Datanbanken, wie der Open Source-Datenbank Apache CouchDB und sogar kompletten Desktopumgebungen, wie GNOME (eine der beliebtesten Desktopumgebungen für GNU/Linux Betriebssysteme) eingesetzt.

Überblick

JavaScript ist eine objektorientierte dynamische Sprache, die verschiedene Typen, Operatoren, Kernobjekte und Methoden besitzt. Ihre Syntax hat Ähnlichkeiten mit Java und C und viele struktuelle Eigenschaften sind sogar gleich. Ein großer Unterschiede zu anderen bekannten Programmiersprachen ist, dass bei JavaScript keine Klassen existieren und stattdessen Objekt-Prototypen zum Einsatz kommen. Ein weiterer großen Unterschied ist, dass Funktionen bei JavaScript auch Objekte sind, wodurch sie ausführbaren Code enthalten und wie Objekte übergeben werden können.

Schauen wir uns zunächst die grundlegenden Komponenten jeder Programmiersprache an: Datentypen und Werte. JavaScript-Programme verarbeiten Werte, die alle einen bestimmten Datentyp haben. Bei JavaScript existieren die folgenden Datentypen:

... und nicht zu vergessen die etwas seltsamen Typen undefined und null; außerdem gibt es noch Arrays, die eine spezielle Art von Objekt sind. Außerdem Dates und Regular Expressions, die ebenfalls Objekte sind. Vom technischen Standpunkt aus betrachtet müssen auch Funktionen hinzugezählt werden, da diese auch nur eine spezielle Art von Objekten sind. Die Aufzählung müsste korrekterweise also in etwa wie folgt aussehen:

  • Number (numerische Werte)
  • String (Zeichenketten)
  • Boolean (boolesche Werte)
  • Object (Objekte)
    • Function (Funktionen)
    • Array (Arrays)
    • Date (Datums- und Zeitobjekt)
    • RegExp (Reguläre Ausdrücke)
  • Null
  • Undefined

Zusätzlich gibt es noch einige vordefinierte Objekte für die Fehlerbehandlung. Der Neueinstieg wird jedoch um einiges leichter, wenn wir die Details etwas vernachlässigen und uns zunächst mit den wichtigsten Datentypen beschäftigen.

Numbers (numerische Werte)

Numbers sind bei JavaScript laut Spezifikation "doppeltpräzise 64-bit Format IEEE 754 Werte". Bei JavaScript existiert kein eigener Typ für Ganzzahlen (Integer), darum sollte man bei der Anwendung von Arithmetik etwas vorsichtig sein, falls man das Rechnen in C oder Java gewöhnt ist. Halten Sie nach Stellen wie dieser Ausschau und beachten Sie die Unterschiede:

0.1 + 0.2 == 0.30000000000000004

In der Praxis werden Integer-Werte wie 32-Bit-Integer behandelt (und werden bei einigen Browser-Implementierungen auch als solche gespeichert), was bei bitweisen Operationen von Bedeutung sein kann. Für weitere Details, lesen Sie das komplette Kapitel über Numbers in der JavaScript-Referenz.

Unterstützt werden die standardmäßigen Rechenoperationen wie z.B. Addition, Subtraktion, Modulo und so weiter. Außerdem existiert noch ein Objekt Math für die Anwendung von mathematischen Funktionen und Konstanten, welches in der Aufzählung oben noch nicht genannt wurde:

Math.sin(3.5);
var d = Math.PI * r * r;

Numerische Werte, die als Strings hinterlegt sind, lassen sich mit Hilfe der vordefinierten Funktion parseInt() zu Ganzzahlen konvertieren. Dabei sollte der Funktion - obwohl dies optional ist - immer die Basis für die Umwandlung als zweites Argument übergeben werden:

> parseInt("123", 10)
123
> parseInt("010", 10)
10

Gibt man die Basis nicht an, könnte dies in älteren Browsern zu überraschenden Ergebnissen führen:

> parseInt("010")
8

Der Grund für dieses Ergebnis ist, dass die Funktion parseInt() den String aufgrund der führenden 0 als oktal interpretiert.

Zur Umwandlung einer Binärzahl zu einer Ganzzahl ändert man einfach die Basis:

> parseInt("11", 2)
3

Auf die gleiche Weise lassen sich auch Gleitkommazahlen mit Hilfe der Funktion parseFloat() konvertieren, welche - anders als die verwandte Funktion parseInt() - immer mit der Basis 10 arbeitet.

Der Operator + kann auch zur Konvertierung von Stringwerten zu Numbers eingesetzt werden, indem er dem String vorangestellt wird:

> + "42"
42

Ein spezieller Wert mit der Bezeichnung NaN (Not a Number) wird zurückgegeben, falls es sich bei dem angegebenen String um keinen numerischen Wert handelt:

> parseInt("hello", 10)
NaN

NaN wirkt geradezu ansteckend bei Rechenoperationen, denn wird NaN bei einer beliebigen Rechnenoperation als Eingabewert angegeben, so ist das Ergebnis ebenfalls NaN:

> NaN + 5
NaN

Mit der vordefinierten Funktion isNaN() lässt sich überprüfen, ob es sich um einen NaN-Wert handelt:

> isNaN(NaN)
true

JavaScript kennt außerdem die speziellen Werte Infinity und -Infinity:

> 1 / 0
Infinity
> -1 / 0
-Infinity

Mit der Funktion isFinite() lässt sich feststellen, ob ein Wert Infinity, -Infinity oder NaN ist:

> isFinite(1/0)
false
> isFinite(-Infinity)
false
> isFinite(NaN)
false

Anmerkung: Die Funktionen parseInt() und parseFloat() parsen einen String, bis ein Zeichen erreicht wird, welches für das entsprechende Number-Format unzulässig ist und geben die Zahl bis zu dieser Stelle zurück. Der unäre + Operator konvertiert den String einfach zu NaN, falls dieser ein unerlaubtes Zeichen enthält. Damit Sie diese Funktionen besser verstehen, probieren Sie alle beschriebenen Methoden am besten selbst aus und versuchen Sie den String "10.2anc" in der Konsole zu parsen.

 

Strings (Zeichenketten)

Strings sind bei JavaScript eine Folge von Zeichen. Um genau zu sein handelt es sich um Unicode-Zeichen, wobei jedes Zeichen durch eine 16-Bit Zahl repräsentiert wird. Falls Sie schon einmal mit Internationalisierung zutun hatten, werden Sie darüber sicher erfreut sein.

Es gibt bei JavaScript keinen speziellen Datentyp für einzelne Zeichen, stattdessen sollte einfach ein String mit einer Länge von 1 verwendet werden.

Um die Länge (Anzahl der Zeichen) eines Strings herauszufinden, greift man auf die Eigenschaft length zurück:

> "hello".length
5

Auch Strings sind Objekte, die Methoden besitzen:

> "hello".charAt(0)
h
> "hello, world".replace("hello", "goodbye")
goodbye, world
> "hello".toUpperCase()
HELLO

Weitere Datentypen und Werte

JavaScript unterscheidet zwischen null, wobei es sich um ein Objekt vom Typ 'object' handelt, welches einen "nicht Wert" repräsentiert und undefined, wobei es sich um ein Objekt vom Typ 'undefined' handelt, welches für einen nicht-initialisierten Wert steht - also anzeigt, dass kein Wert zugewiesen wurde. Später werden wir uns noch gründlicher mit Variablen beschäftigen. An dieser Stelle nur noch einmal zur Verdeutlichung: Bei JavaScript ist es möglich, eine Variable zu deklarieren, ohne sie zu initialisieren, ihr also einen Wert zuzuweisen - und genau dann ist ihr Typ undefined (undefiniert).

JavaScript besitzt einen booleschen Typ, welcher mit den Werten true und false arbeitet (die beide Schlüsselwörter sind). Jeder Wert kann nach den folgenden Regeln zu einem booleschen Wert konvertiert werden:

  • false, 0, der leere String (""), NaN, null, und undefined werden alle zu false
  • Alle anderen Werte werden true

Die Konvertierung kann explizit mit der Funktion Boolean() durchgeführt werden:

> Boolean("")
false
> Boolean(234)
true

Das ist jedoch meist unnötig, da JavaScript die Konvertierung automatisch vornimmt, wenn ein boolescher Wert erwartet wird, wie z.B. bei einer if-Anweisung (siehe unten). Aus diesem Grund ist oft von "true Werten" und "false Werten" die Rede, womit Werte gemeint sind, die zu true oder false werden, nachdem sie zu booleschen Werten konvertiert wurden.

Boolesche Operationen wie beispielsweise && (logisch UND), || (logisch ODER) und ! (logisch NICHT) werden ebenfalls unterstützt (siehe unten).

Variablen

Neue Variablen werden bei JavaScript mit dem Schlüsselwort var deklariert:

var a;
var name = "Simon";

Deklariert man eine Variable, ohne ihr einen Wert zuzuweisen, ist ihr Typ undefined.

Ein sehr wichtiger Unterschied zu anderen Programmiersprachen besteht darin, dass bei JavaScript Blöcke keine Sichtbarkeitsbereiche festlegen. Wird z.B. eine Variable mit dem var-Schlüsselwort in einer Kontrollstruktur (z.B. if-Anweisung) definiert, so ist diese Variable innerhalb der ganzen Funktion sichtbar - und nicht nur innerhalb des if-Blocks.

Operatoren

Die numerischen Operatoren sind +, -, *, / und % (Modulo). Werte werden mit dem Gleichzeichen = zugewiesen. Außerdem gibt es noch kombinierte Operatoren wie += und -=, welche wie x = x Operator y interpretiert werden.

x += 5
x = x + 5

Zum Inkrementieren und Dekrementieren von Werten kann man die Operatoren ++ und -- verwenden. Je nachdem, ob der Operator der Variablen vorangestellt oder hinten angehängt wird, wird zwischen Postdekrement bzw. -inkrement und Präinkrement bzw. -dekrement unterschieden.

Mit dem + Operator ist es auch möglich Strings zu verbinden:

> "hello" + " world"
hello world

Addiert man einen String mit einer Number (oder einem anderen Wert), wird alles zuerst zu einem String konvertiert:

> "3" + 4 + 5       // "3" + 4 = "34"; "34" + 5 = "345"
345
> 3 + 4 + "5"
75

Die Verbindung zweier Werte mit dem Pluszeichen ist für die Konvertierung von Werten sehr nützlich.

Vergleiche können bei JavaScript mit den Operatoren <, >, <= und >= vorgenommen werden. Diese Operatoren funktionieren sowohl bei Strings als auch bei Numbers. Das Testen auf Gleichheit ist etwas komplizierter. Mit dem Operator == (doppeltes Gleichzeichen) werden beim Vergleich die Typen der Werte nicht berücksichtigt und die Ergebnisse sind zum Teil überraschend:

> "dog" == "dog"
true
> 1 == true
true

Damit unterschiedliche Datentypen berücksichtigt werden, muss dem Operator ein weiteres Gleichzeichen hinzugefügt werden:

> 1 === true
false
> true === true
true

Des Weiteren gibt es die Operatoren  != und !== für das Testen auf Ungleichheit.

Außerdem gibt es bei JavaScript noch biweise Operationen.

Kontrollstrukturen

Die Kontrollstrukturen sind bei JavaScript ähnlich wie bei anderen Sprachen der C-Familie. Bedingte Anweisungen und Verzweigungen können mit if und else durchgeführt werden. Durch Verknüpfung der beiden Codewörter können auch mehrfache Verzweigungen/Fallunterscheidungen programmiert werden:

var name = "kittens";
if (name == "puppies") {
  name += "!";
} else if (name == "kittens") {
  name += "!!";
} else {
  name = "!" + name;
}
name == "kittens!!"

Es gibt while- und do-while-Schleifen. Die while-Schleife wird für die Programmierung einfacher Schleifen eingesetzt, während bei do-while der Code im Körper der Schleife bereits vor Überprüfung der Bedingung einmal ausgeführt wird.

while (true) {
  // Endlosschleife!
}

var input;
do {
  input = get_input();
} while (inputIsNotValid(input))

Die for-Schleife ist wie bei C und Java: Die Kontrollinformationen können in einer einzigen Zeile angegeben werden.

for (var i = 0; i < 5; i++) {
  // Verarbeitet 5 Elemente
}

Die Operatoren && und || arbeiten mit Kurzschlusslogik - ob der zweite Operand ausgewertet wird, hängt vom Ergebnis der Auswertung des ersten Operanden ab. Dieses Verhalten ist nützlich für die Überprüfung auf null, bevor auf die Attribute eines Objekts zugegriffen wird:

var name = o && o.getName();

Und auch zum Festlegen von Default-Werten:

var name = otherName || "default";

JavaScript besitzt einen ternären Operator für Vergleichausdrücke:

var allowed = (age > 18) ? "yes" : "no";

Auch die switch-Anweisung kann zur Programmierung von Verzweigungen/ Fallunterscheidungen eingesetzt werden:

switch(action) {
    case 'draw':
        drawit();
        break;
    case 'eat':
        eatit();
        break;
    default:
        donothing();
}

Lässt man die break-Anweisung am Ende eines case-Abschnitts weg, werden auch die folgenden Abschnitte ausgeführt, bis die Ausführung auf ein break trifft oder das Ende der switch-Anweisung erreicht ist. Dieses Verhalten ist eher selten erwünscht und daher ist es empfehlenswert mit einem Kommentar darauf hinzuweisen, falls die break-Anweisung absichtlich weggelassen wird, um später das Debugging zu erleichtern:

switch(a) {
    case 1: // fallthrough
    case 2:
        eatit();
        break;
    default:
        donothing();
}

Der default-Abschnitt ist optional. Auch Ausdrücke können als Argument für den switch-Teil und die case-Bedingungen eingesetzt werden. Vergleiche werden wie beim Operator === durchgeführt:

switch(1 + 3) {
    case 2 + 2:
        yay();
        break;
    default:
        neverhappens();
}

Objekte

JavaScript-Objekte setzen sich aus einfachen Name-Wert-Paaren zusammen; sind also äquivalent zu:

  • Dictionaries bei Python
  • Hashes bei Perl und Ruby
  • Hashtabellen bei C und C++
  • HashMaps bei Java
  • Assoziative Arrays bei PHP

Einer der Hauptgründe für die weite Verbreitung und Beliebtheit dieser Datenstruktur, ist ihre Vielseitigkeit und Flexibilität. Da bei JavaScript alles ein Objekt ist, muss sehr oft in Hashtaballen gesucht werden. Zum Glück läuft diese Suche bei JavaScript sehr schnell ab.

Der "Name"-Teil ist ein JavaScript-String, während als Wert ein beliebiger für JavaScript gültiger Wert gesetzt werden kann - andere Objekte eingeschlossen. Dies ermöglicht die die Zusammenstellung von beliebig komplexen Datenstrukturen.

Es gibt zwei verschiedene Möglichkeit zur Erzeugung von leeren Objekten. Zuerst die "alte" Schreibweise:

var obj = new Object();

Und nun die "neue" Syntax:

var obj = {};

Die Semantik beider Schreibweisen ist äquivalent. Die zweite Schreibweise wird "objektliterale Syntax" genannt. Sie ist kompakter und praxistauglicher, wird auch für JSON verwendet und sollte in jedem Fall bevorzugt eingesetzt werden.

Nachdem ein Objekt einmal erstellt wurde, kann auf die Eigenschaften des Objekts mit einer der beiden folgenden Methoden zugegriffen werden.

Über die Punkt-Notation:

obj.name = "Simon";
var name = obj.name;

Oder über die Klammer-Notation:

obj["name"] = "Simon";
var name = obj["name"];

Auch diese beiden Varianten sind semantisch gleich. Die zweite Schreibweise hat allerdings den Vorteil, dass der Name der Eigenschaft als String zur Verfügung gestellt wird und diese zur Laufzeit verarbeitet werden. Allerdings werden so auch einige Optimierungen der JavaScript-Engine nicht durchgeführt.

Die Syntax kann außerdem dazu verwendet werden, um Eigenschaften abzufragen und zu setzen, die wie reservierte Schlüsselwörter benannt sind:

obj.for = "Simon"; // Syntaxfehler, da 'for' ein reserviertes Codewort ist
obj["for"] = "Simon"; // works fine

Mit der objektliteralen Syntax können Objekte komplett mit allen zugehörigen Eigenschaften initialisiert werden:

var obj = {
    name: "Carrot",
    "for": "Max",
    details: {
        color: "orange",
        size: 12
    }
}

Der Zugriff auf Eigenschaften ist durch Verkettung der Eigenschaften möglich:

> obj.details.color
orange
> obj["details"]["size"]
12

Arrays

Arrays sind bei JavaScript eine spezielle Art von Objekten. Sie funktionieren weitgehend wie normale Objekte (numerische Eigenschaften können nur über die []-Syntax angesprochen werden), besitzen jedoch eine zusätzliche Eingenschaft length. Der Wert dieser Eigenschaft entspricht immer dem höchsten Index des Arrays + 1.

Die alte Syntax für die Erstellung von Arrays ist wie folgt:

> var a = new Array();
> a[0] = "dog";
> a[1] = "cat";
> a[2] = "hen";
> a.length
3

Die arrayliterale Syntax bietet mehr Komfort:

> var a = ["dog", "cat", "hen"];
> a.length
3

Ein Komma am Ende eines Arrayliterals wird von Browsern unterschiedlich interpretiert und sollte daher vermieden werden.

Beachten Sie, dass array.length nicht unbedingt der Anzahl der Elemente des Arrays entspricht. Schauen Sie sich hierzu folgendes Beispiel an:

> var a = ["dog", "cat", "hen"];
> a[100] = "fox";
> a.length
101

Wir erinnern uns: der Wert von length entspricht immer dem höchsten Index + 1.

Versucht man auf einen nicht-existenten Array-Index zuzugreifen, erhält man als Ergebnis undefined:

> typeof a[90]
undefined

Die Eigenschaft length kann man sich zum Iterieren über ein Array zunutze machen:

for (var i = 0; i < a.length; i++) {
    // Verarbeitung von a[i]
}

Diese Methode ist jedoch etwas ineffizent, da bei jeder Iteration erneut der Wert von length überprüft wird. Etwas effizienter ist es, wenn wir den Wert von length in einer Variablen speichern:

for (var i = 0, len = a.length; i < len; i++) {
    // Verarbeitung von a[i]
}

Noch effizienter ist die folgende Methode:

for (var i = 0, item; item = a[i++];) {
    // Verarbeitung von item
}

Bei dieser Methode werden zwei Variablen, i und item erstellt. Die Zuweisung im mittleren Teil der for-Schleife wird vor jedem Durchlauf auf ihren Wahrheitswert (booleschen Wert) hin überprüft - ist der Wert true, wird die Schleife fortgesetzt. Wenn ein "falsches" Element gefunden wird, wie z.B. undefined, bricht die Schleife ab. Die Variable i wird bei jedem Durchlauf inkrementiert und die Elemente des Arrays werden so nacheinander der Variablen item zugewiesen.

Da bei dieser Methode die einzelnen Elemente zusätzlich auf ihren Wahrheitswert überprüft werden, sollte sie nur für Arrays eingesetzt werden, die keine "falschen" Werte enthalten (z.B. Arrays mit Objekten oder DOM-Nodes). Falls man über numerische Daten iteriert, die 0 enthalten können oder String-Daten mit leeren Strings, sollte man besser die vorherige Methode ( mit i und len) einsetzen.

Mit der for...in-Schleife gibt es eine weitere Möglichkeit zum Iterieren. Hier ist zu beachten, dass neue Eigenschaften, die evtl. später Array.prototype hinzugefügt werden, ebenfalls iteriert werden:

for (var i in a) {
  // Verarbeiten von a[i]
}

Der sicherste Weg, um Elemente einem Array hinzuzufügen ist wie folgt:

a[a.length] = item;     // genau wie a.push(item);

Da a.length immer eins höher ist als der höchste Index, kann man sicher sein, dass der Wert einer freien Position am Ende des Arrays zugewiesen wird.

Arrays besitzen verschiedene Methoden:

Methodenname Beschreibung
a.toString()  
a.toLocaleString()  
a.concat(item[, itemN]) Gibt ein neues Array, dem die Elemente hinzugefügt wurden, zurück
a.join(sep)  
a.pop() Entfernt das letzte Element und gibt es zurück.
a.push(item[, itemN]) Push fügt eines oder mehrere Elemente am Ende hinzu.
a.reverse()  
a.shift()  
a.slice(start, end) Gibt einen Teil eines Arrays zurück.
a.sort([cmpfn]) Nimmt eine Funktion für den Vergleich entgegen.
a.splice(start, delcount[, itemN]) Modifiziert ein Array, indem ein Teil gelöscht und durch mehr Elemente ersetzt wird.
a.unshift([item]) Fügt Elemente am Anfang hinzu.

Funktionen

Neben Objekten, gehören auch Funktionen zu den Kernkomponenten von JavaScript. Die Syntax für eine einfache Basisfunktion könnte kaum simpler sein:

function add(x, y) {
    var total = x + y;
    return total;
}

Diese Funktionen beinhaltet alles, was man zum Verstehen von einfachen Funktionen wissen muss. Einer JavaScript-Funktion können benannte Argumente übergeben werden, welche dann innerhalb der Funktion verarbeitet werden können. Der Funktionskörper darf beliebig viele Anweisungen enthalten und innerhalb des Funktionsblocks können eigene lokale Variablen definiert werden, die nur innerhalb der Funktion zugänglich sind. Mit der return-Anweisung wird der Rückgabewert festgelegt und die Funktion an belibiger Stelle terminiert. Wird keine return-Anweisung angegeben (oder kein Rückgabewert), gibt JavaScript undefined zurück.

Die benannten Parameter sind eher wie Richtlinien und nicht etwa wie Pflichtangaben zu verstehen. Eine Funktion kann also auch ohne Angabe der erwarteten Parameter aufgerufen werden. Die Werte dieser Parameter sind dann undefined.

> add()
NaN // Addition mit undefined nicht möglich

Der Funktion können auch mehr als nur die erwarteten Paramenter übergeben werden:

> add(2, 3, 4)
5 // Addition der ersten beiden Parameter, der dritte wird ignoriert

Auf den ersten Blick mag es etwas komisch erscheinen, doch Funktionen haben innerhalb des Funktionskörpers Zugriff auf eine zusätzliche Variable mit dem Namen arguments, wobei es sich um ein arrayähnliches Objekt handelt, welches alle Werte enthält, die an die Funktion übergeben wurden. Hier eine abgeänderte Version unserer add()-Funktion, die beliebig viele Werte entgegennimmt, über arguments auf diese Werte zugreift und sie addiert:

function add() {
    var sum = 0;
    for (var i = 0, j = arguments.length; i < j; i++) {
        sum += arguments[i];
    }
    return sum;
}

> add(2, 3, 4, 5)
14

Die Verwendung dieser Funktion ist allerdings nicht viel komfortabler als wenn man einfach 2 + 3 + 4 + 5 schreiben würde. Das ändert sich jedoch, wenn die Funktion komplexere Aufgaben erledigen soll. Hier eine Funktion für die Berechnung eines Durchschnitts:

function avg() {
    var sum = 0;
    for (var i = 0, j = arguments.length; i < j; i++) {
        sum += arguments[i];
    }
    return sum / arguments.length;
}
> avg(2, 3, 4, 5)
3.5

Diese Funktion ist sehr praktisch, weist jedoch ein neues Problem auf: Die avg()-Funktion nimmt eine durch Kommas getrennte Argumentliste entgegen. Doch wie schaffen wir es, dass mit der Funktion auch der Durchschnitt für Werte eines Arrays berechnet werden kann?

Wir könnten die Funktion etwas umschreiben:

function avgArray(arr) {
    var sum = 0;
    for (var i = 0, j = arr.length; i < j; i++) {
        sum += arr[i];
    }
    return sum / arr.length;
}
> avgArray([2, 3, 4, 5])
3.5

Es wäre jedoch besser, wenn wir die erste Funktion wiederverwenden könnten. Glücklicherweise ist es bei JavaScript möglich, eine Funktion mit einem Array von Argumenten aufzurufen, indem man auf die Methode apply() zurückgreift, die alle Function-Objekte besitzen:

> avg.apply(null, [2, 3, 4, 5])
3.5

Das zweite Argument der apply()-Funktion ist das Array mit den Argumenten für die Funktion; das erste Argument (null) wird später erklärt. An diesem Stelle sollte deutlich werden, dass auch Funktionen Objekte sind.

JavaScript erlaubt die Erstellung von sogenannten anonymen Funktionen:

var avg = function() {
    var sum = 0;
    for (var i = 0, j = arguments.length; i < j; i++) {
        sum += arguments[i];
    }
    return sum / arguments.length;
}

Die Semantik dieser Funktion ist äquivalent zur "function avg()"-Schreibweise. Anonyme Funktionen lassen sich überall dort einsetzen, wo man normalerweise einen Ausdruck verwenden würde. Dadurch sind eine ganze Reihe verschiedener Tricks möglich. So lassen sich z.B. lokale Variablen "verstecken", wie bei Block Scopes in C:

> var a = 1;
> var b = 2;
> (function() {
    var b = 3;
    a += b;
})();
> a
4
> b
2

JavaScript erlaubt den rekursiven Aufruf von Funktionen, was bei Baumstrukturen, wie z.B. dem DOM von Browsern, sehr von Vorteil sein kann.

function countChars(elm) {
    if (elm.nodeType == 3) { // TEXT_NODE
        return elm.nodeValue.length;
    }
    var count = 0;
    for (var i = 0, child; child = elm.childNodes[i]; i++) {
        count += countChars(child);
    }
    return count;
}

Hier stoßen wir auf ein mögliches Problem bei anonymen Funktionen: Wie ruft man die Funktion rekursiv auf, wenn sie keinen Namen besitzt? Eine Antwort könnte im arguments Objekt versteckt sein, denn dieses Objekt besitzt eine Eigenschaft arguments.callee. Die Eigenschaft gilt jedoch als veraltet und im strict-Mode ist der Zugriff sogar gesperrt. Stattdessen sollten hier IIFEs (Immediately Invoked Function Expressions) wie folgt eingesetzt werden:

var charsInBody = (function counter(elm) {
    if (elm.nodeType == 3) { // TEXT_NODE
        return elm.nodeValue.length;
    }
    var count = 0;
    for (var i = 0, child; child = elm.childNodes[i]; i++) {
        count += counter(child);
    }
    return count;
})(document.body);

Der Name der anonymen Funktion ist nur innerhalb dem Sichtbarkeitsbereich der Funktion verfügbar (oder sollte es wenigstens sein). Diese Vorgehensweise erlaubt der JavaScript-Engine den Code besser zu optimieren und auch die Lesbarkeit des Codes wird so verbessert.

Benutzerdefinierte Objekte

Anmerkung: Detaillierte Informationen zu objektorientiertem Programmieren in JavaScript finden Sie in der Einführung zu objektorientiertem Programmieren in JavaScript.

Beim klassischen objektorientierten Programmieren sind Objekte Zusammenstellungen von Daten und Methoden, welche diese Daten verarbeiten. JavaScript ist jedoch eine prototypenbasierte Sprache, die - anders als Java oder C++ - keine Klassen besitzt. Anstatt Klassen werden bei JavaScript Funktionen eingesetzt. Angenommen es existiert ein Objekt person mit zwei Feldern, jeweils für den Vor- und Nachnamen und wir wollen den Namen auf einer Website anzeigen lassen. Namen werden üblicherweise in zwei verschiedenen Varianten geschrieben: Entweder als "Vorname Nachname" oder mit dem Nachnamen zuerst, also "Nachname, Vorname". Greifen wir auf die beschriebenen Funktionen und Objekte zurück, lässt sich dies z.B. so umsetzen:

function makePerson(first, last) {
    return {
        first: first,
        last: last
    }
}
function personFullName(person) {
    return person.first + ' ' + person.last;
}
function personFullNameReversed(person) {
    return person.last + ', ' + person.first;
}
> s = makePerson("Simon", "Willison");
> personFullName(s)
Simon Willison
> personFullNameReversed(s)
Willison, Simon

Das funktioniert zwar, ist aber keine sehr elegante Lösung, denn so befinden sich am Ende dutzende verschiedene Funktionen im globalen Namensraum. Was wir also suchen ist eine Möglichkeit, um eine Funktion an ein Objekt anzuhängen. Da Funktionen bei JavaScript bekanntlich auch Objekte sind, lässt sich dies wie folgt bewerkstelligen:

function makePerson(first, last) {
    return {
        first: first,
        last: last,
        fullName: function() {
            return this.first + ' ' + this.last;
        },
        fullNameReversed: function() {
            return this.last + ', ' + this.first;
        }
    }
}
> s = makePerson("Simon", "Willison")
> s.fullName()
Simon Willison
> s.fullNameReversed()
Willison, Simon

In diesem Code gibt es etwas Neues zu entdecken: Das this-Schlüsselwort; wird es innerhalb einer Funktion benutzt, verweist es auf das aktuelle Objekt. Welches Objekt als das Aktuelle gilt, hängt davon ab, wie die Funktion aufgerufen wurde. Wenn die Funktion über ein Objekt unter Benutzung der Punkt- oder Klammer-Notation aufgerufen wurde, repräsentiert this dieses Objekt. Wurde zum Aufruf die Punkt-Notation nicht verwendet, dann verweist 'this' auf das globale Objekt. Dieses Verhalten führt oft zu Verwirrung und ist eine bekannte Fehlerursache. Zum besseren Verständnis hier also ein weiteres Beispiel:

> s = makePerson("Simon", "Willison")
> var fullName = s.fullName;
> fullName()
undefined undefined

Beim Aufruf der Funktion fullName() verweist 'this' auf das globale Objekt. Da keine globalen Variablen mit der Bezeichnung 'first' oder 'last' existieren, erhalten wir für beide undefined.

Das this-Schlüsselwort können wir uns zunutze machen, um die makePerson-Funktion weiter zu verbessern:

function Person(first, last) {
    this.first = first;
    this.last = last;
    this.fullName = function() {
        return this.first + ' ' + this.last;
    }
    this.fullNameReversed = function() {
        return this.last + ', ' + this.first;
    }
}
var s = new Person("Simon", "Willison");

In diesem Code ist ein weiteres Schlüsselwort enthalten: 'new'. Dieses Schlüsselwort ist mit 'this' verwandt. Es erstellt ein neues Objekt und ruft die nachstehende Funktion auf, wobei 'this' dann das neue Objekt repräsentiert. Funktionen, die für den Aufruf mit new erstellt wurden, werden Konstruktorfunktionen genannt. Zur leichteren Erkennung dieser für new bestimmten Konstruktorfunktionen hat man sich darauf geeinigt, den Namen mit einem Großbuchstaben beginnen zu lassen.

Unser Objekt person reift so langsam heran, weißt jedoch noch immer einige Ungereimtheiten auf: Bei jeder Erstellung eines person-Objekts werden jeweils auch immer zwei neue Funktionsobjekte erzeugt. Wäre es nicht viel besser, wenn dieser Code wiederverwendet würde?

function personFullName() {
    return this.first + ' ' + this.last;
}
function personFullNameReversed() {
    return this.last + ', ' + this.first;
}
function Person(first, last) {
    this.first = first;
    this.last = last;
    this.fullName = personFullName;
    this.fullNameReversed = personFullNameReversed;
}

Das bringt uns einen Schritt weiter, denn so werden die Funktionen für die Methoden des Objekts nur einmal erstellt und im Konstruktor darauf referenziert.

Es sind noch weitere Verbesserungen möglich:

function Person(first, last) {
  this.first = first;
  this.last = last;
} 
Person.prototype.fullName = function() {
  return this.first + ' ' + this.last; 
}
Person.prototype.fullNameReversed = function() {
  return this.last + ', ' + this.first;
}

Person.prototype ist ein Objekt, das sich alle Person-Instanzen teilen. Damit wird eine Art Prototype-Suchkette (engl. Prototype chain) gebildet: Bei jedem Versuch, auf eine Eigenschaft von Person zuzugreifen, die nicht direkt für das Person-Objekt existiert, wird überprüft, ob bei Person.prototype diese Eigenschaft vorhanden ist. Demzufolge ist alles, was Person.prototype zugewiesen wird, für alle Instanzen dieses Konstruktors über this verfügbar.

Prototypen sind ein sehr mächtiges Werkzeug. Der Prototype eines Objekts kann innerhalb eines Scripts jederzeit verändert werden, also können auch bereits existierenden Instanzen neue Methoden während der Laufzeit hinzugefügt werden:

> s = new Person("Simon", "Willison");
> s.firstNameCaps();
TypeError on line 1: s.firstNameCaps is not a function
> Person.prototype.firstNameCaps = function() {
    return this.first.toUpperCase()
}
> s.firstNameCaps()
SIMON

Interessanterweise können auch den von JavaScript vordefinierten Objekten neue Methoden und Eigenschaften hinzugefügt werden. So können wir z.B. dem vordefinierten String-Objekt eine neue Methode hinzufügen, womit der String umgekehrt ausgegeben werden kann:

> var s = "Simon";
> s.reversed()
TypeError on line 1: s.reversed is not a function
> String.prototype.reversed = function() {
    var r = "";
    for (var i = this.length - 1; i >= 0; i--) {
        r += this[i];
    }
    return r;
}
> s.reversed()
nomiS

Die neue Methode funktioniert sogar mit String-Literalen!

> "This can now be reversed".reversed()
desrever eb won nac sihT

Wie schon erwähnt, ist der Prototype Teil einer Suchkette. Der Ursprung dieser Kette ist Object.prototype. Der Prototype besitzt unter anderem die Methode toString(), welche aufgerufen wird, wenn ein Objekt als String repräsentiert werden soll, was sich z.B. für das Debuggen unseres Person-Objekts als sehr nützlich erweist:

> var s = new Person("Simon", "Willison");
> s
[object Object]
> Person.prototype.toString = function() {
    return '<Person: ' + this.fullName() + '>';
}
> s
<Person: Simon Willison>

Erinnern Sie sich noch an die Stelle, als wir avg.apply() als erstes Argument null übergeben haben? Hier die Erklärung: Mit dem ersten Argument von apply() wird das Objekt festgelegt, welches von 'this' repräsentiert werden soll. Der folgende Code stellt z.B. eine einfache Implemetierung von 'new' dar:

function trivialNew(constructor) {
    var o = {}; // Create an object
    constructor.apply(o, arguments);
    return o;
}

Das ist keine exakte Replikation von 'new', weil die Prototype-Kette nicht angelegt wird. Das erste Argument wird in der Praxis eher selten benutzt. Dennoch kann es nützlich sein, darüber Bescheid zu wissen.

Eine mit apply() verwandte Funktion ist die Funktioncall, die ebenfalls das setzen von 'this' erlaubt. Statt eines Arrays nimmt sie jedoch eine erweiterte Liste mit Argumenten entgegen.

function lastNameCaps() {
    return this.last.toUpperCase();
}
var s = new Person("Simon", "Willison");
lastNameCaps.call(s);
// Is the same as:
s.lastNameCaps = lastNameCaps;
s.lastNameCaps();

Innere Funktionen

Funktionsdeklarationen sind bei JavaScript auch innerhalb von anderen Funktionen erlaubt. Das lässt sich bei der makePerson()-Funktion sehr gut erkennen. Ein wichtiger Punkt ist, dass innere Funktionen Zugriff auf Variablen haben, die im Sichtbarkeitsbereich der äußeren Funktion liegen:

function betterExampleNeeded() {
    var a = 1;
    function oneMoreThanA() {
        return a + 1;
    }
    return oneMoreThanA();
}

Mit der Unterteilung in Unterfunktionen lässt sich der Code strukturieren und ist so auch besser wartbar. Arbeitet eine Funktion z.B. mit anderen Funktionen, die nur bei dieser Funktion genutzt werden, können diese Funktionen in die Funktion eingebettet werden. Damit lässt sich die Anzahl der Funktionen im globalen Namensraum gering halten, was eigentlich nie verkehrt ist.

Auch globale Variablen, die aufgrund ihrer Verfügbarkeit im ganzen Script dazu verführen zum Austausch von Werten zwischen unterschiedlichen Funktionen eingesetzt zu werden, lassen sich auf diese Weise vermeiden, denn die eingebetteten Funktionen können nun über Variablen der äußeren Funktion Werte austauschen, ohne den globalen Namensraum zu belasten. Diese Technik sollte jedoch mit Vorsicht eingesetzt werden.

Closures

Das bringt uns zu einem der mächtigsten Abstraktionsmechanismen, die JavaScript bereitstellt. Closures zählen leider auch gleichzeiteitig zu den Konzepten, die sehr oft missverstanden werden.

Was sind Closures?

Hier ein Beispiel:

function makeAdder(a) {
    return function(b) {
        return a + b;
    }
}
x = makeAdder(5);
y = makeAdder(20);
x(6)
?
y(7)
?

Der Name der makeAdder-Funktion lässt es schon vermuten: Der Zweck dieser Funktion ist die Erstellung neuer 'adder' Funktionen, die beim Aufruf mit einem Argument den übergebenen Wert mit dem Wert addieren, welcher der makeAdder-Funktion bei der Erstellung der Funktion übergeben wurde.

DasVerhalten ist hier ähnlich wie bei der inneren Funktion von vorher: Eine Funktion, die innerhalb einer anderen Funktion definiert wurde, hat Zugriff auf die Variablen der äußeren Funktion. Es gibt jedoch einen Unterschied. Wenn die äußere Funktion bis zum return durchläuft, könnte man annehmen, dass die lokalen Variablen nicht mehr existieren. Doch diese Variablen bleiben bestehen und existieren weiterhin - andernfalls könnte die adder-Funktion nicht korrekt funktionieren. Außerdem gibt es zwei verschiedene "Kopien" der lokalen Variablen der makeAdder-Funktion - eine mit dem Wert 5 und eine weitere mit dem Wert 20. Die Ergebnisse beim Aufruf der Funktionen sind also wie folgt:

x(6) // returns 11
y(7) // returns 27

JavaScript erstellt bei jedem Aufruf einer Funktion ein Scope-Objekt ("Sichtbarkeitsobjekt"), welches die lokalen Variablen, die innerhalb der Funktion erstellt wurden, aufnimmt. Dieses Objekt wird mit den Variablen initialisiert, die als Parameter an die Funktion übergeben wurden. Das Objekt verhält sich also ungefähr so, wie das globale Objekt, das alle globalen Funktionen und Variablen beinhaltet. Es gibt jedoch einige bemerkenswerte Unterschiede: Zum einen wird bei jedem Aufruf einer Funktion ein neues Scope-Objekt erstellt. Zum anderen kann mit JavaScript-Code auf das scope-Objekt nicht zugegriffen werden, wie beim globalen Objekt (das jederzeit über window angesprochen werden kann). So existiert z.B. auch kein Mechanismus, um über die Eigenschaften des scope-Objekts zu iterieren.

Beim Aufruf der makeAdder-Funktion wird also ein solches Scope-Objekt erstellt, das eine Eigenschaft besitzt: Die Variable a, welche das an die makeAdder-Funktion übergebene Argument darstellt. Die makeAdder-Funktion gibt dann eine neu erstellte Funktion zurück. Normalerweise würde die Garbage Collection von JavaScript das für makeAdder erstellte scope-Objekt an dieser Stelle entsorgen, doch die zurückgegebene Funktion enthält noch immer eine Referenz auf das Scope-Objekt. Und solange noch Referenzen auf das Scope-Objekt existieren, bleibt das Scope-Objekt erhalten.

Scope-Objekte bilden ebenfalls eine Kette, die ähnlich funktioniert wie die Prototype-Kette.

Eine Closure ist eine Kombination aus einer Funktion und dem Scope-Objekt, in dem die Funktion erstellt wurde.

Closures erlauben das Speicherung von Zuständen und können daher oft anstelle von Objekten eingesetzt werden.

Speicherlecks (Memory leaks)

Ein unvorteilhafter Seiteneffekt beim Einsatz von Closures tritt beim Internet Exporer auf. JavaScript ist eine Sprache, die mittels einer Garbage Collection den Speicher aufräumt. Den Objekten wird bei ihrer Erstellung also Speicher zugewiesen und dieser wieder freigegeben, sobald keine Referenzen mehr auf das Objekt existieren. Objekte, welche durch die Hostumgebung zur Verfügung gestellt werden, werden in dieser Umgebung verwaltet.

Die Browser müssen also eine große Menge von Objekten verwalten, welche die angezeigte HTML-Seite repräsentieren - die Objekte des DOM. Es ist Sache des Browsers, Speicher für diese Objekte zu alloziieren und wieder freizugeben.

Der Internet Explorer verwendet ein eigenes Schema für die Garbage Collection, getrennt von dem Mechanismus, der für JavaScript verwendet wird. Es ist die Interaktion zwischen diesen beiden Systemen, die zu Speicherlecks führen kann.

Ein Speicherleck tritt beim Internet Explorer immer dann auf, wenn sich JavaScript-Objekte und native Objekte gegenseitig referenzieren. Hier ein Beispiel:

function leakMemory() {
    var el = document.getElementById('el');
    var o = { 'el': el };
    el.o = o;
}

Die gegenseitige Referenz, welche durch diesen Code entsteht, führt zu einem Speicherleck; Der Internet Explorer gibt den Speicher für el und o erst wieder frei, wenn der Browser komplett beendet wird.

Das Speicherleck bei dem oben gezeigten Code würde wahrscheinlich von Anwendern selbst kaum bemerkt werden, da die Auswirkungen von Speicherlecks erst zum Vorschein kommen, wenn viel Speicher belegt wird. Das kann zum Beispiel beim Einsatz von großen Datenstrukturen der Fall sein oder wenn mit Schleifen gearbeitet wird.

Speicherlecks sind selten so offensichtlich wie hier - zumeist bestehen komplexe Datenstrukturen aus mehreren Schichten von Referenzen, wodurch gegenseitige Referenzen schwer erkennbar werden.

Mit Closures kann man sehr leicht unabsichtlich ein Speicherleck erzeugen:

function addHandler() {
    var el = document.getElementById('el');
    el.onclick = function() {
        this.style.backgroundColor = 'red';
    }
}

Der Code bewirkt, dass der Hintergrund des Elements el beim Anklicken rot eingefärbt wird. Gleichzeitig wird durch diesen Code ein Speicherleck produziert. Der Grund dafür ist, dass die Referenz auf el in der Closure bestehen bleibt, die für die anonyme Funktion erstellt wurde. Hierdurch entsteht eine gegenseitige Referenzierung zwischen dem JavaScript-Objekt (der Funktion) und dem nativen Objekt (el).

Für die Behebung dieses Problems gibt es einige Workarounds. Die einfachste Lösung ist, die Variable el erst gar nicht zu verwenden:

function addHandler(){
    document.getElementById('el').onclick = function(){
        this.style.backgroundColor = 'red';
    }
}

Seltsamerweise lässt sich das Problem auch durch das Hinzufügen einer weiteren Closure beheben:

function addHandler() {
    var clickHandler = function() {
        this.style.backgroundColor = 'red';
    };
    (function() {
        var el = document.getElementById('el');
        el.onclick = clickHandler;
    })();
}

Die innere Funktion wird hier direkt ausgeführt und der Inhalt dieser Funktion dabei vor der mit clickHandler erstellten Closure versteckt.

Ein weiterer guter Trick zur Vermeidung von gegenseitigen Referenzen ist das Auflösen über das window.onunload-Event. Viele Event-Bibliotheken erledigen dies automatisch. Dadurch wird allerdings unter Firefox 1.5 bfcache deaktiviert und aus diesem Grund sollte ein unload-Listener nur dann eingesetzt werden, wenn noch andere Gründe dafür sprechen.

Original Document Information

  • Author: Simon Willison
  • Last Updated Date: March 7, 2006
  • Copyright: © 2006 Simon Willison, contributed under the Creative Commons: Attribute-Sharealike 2.0 license.
  • More information: For more information about this tutorial (and for links to the original talk's slides), see Simon's Etech weblog post.

 

Schlagwörter des Dokuments und Mitwirkende

Schlagwörter: 
Mitwirkende an dieser Seite: fscholz, eminor
Zuletzt aktualisiert von: fscholz,
Seitenleiste ausblenden