Eine Wiedereinführung in JavaScript

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. Heutzutage wird JavaScript in vielen Applikationen verwendet, die von einer breiten Konsumentenschicht benützt werden. Dies zeigt, dass sich tiefgreifendes Fachwissen in diesem Bereich für jeden Web- und App-Entwickler 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 Sprachen 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 Standardisierungsorganisation, 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 politischen Meinungsverschiedenheiten zur Komplexität der Sprache fallen gelassen, stellte jedoch eine Basis bei der Entwicklung der fünften und sechsten Edition des Standards dar, welche im Dezember 2009 bzw. im Juni 2015 veröffentlicht wurde.

Aus Gründen der Vertrautheit verwenden wir ab jetzt die Bezeichnung "JavaScript" anstatt "ECMAScript".

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-Datenbanken, 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, vordefinierte Objekte und Methoden besitzt. Ihre Syntax hat Ähnlichkeiten mit Java und C und viele strukturelle Eigenschaften sind sogar gleich. Ein großer Unterschiede zu anderen bekannten Programmiersprachen ist, dass in JavaScript keine Klassen existieren und stattdessen Prototypen zum Einsatz kommen. Ein weiterer großer Unterschied ist, dass Funktionen bei JavaScript auch Objekte sind, wodurch man beispielsweise bei der Parameterübergabe mit Funktionen so einfach wie mit Objekten umgehen kann.

Schauen wir uns zunächst die grundlegenden Komponenten jeder Programmiersprache an: Datentypen. 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 Regulären Ausdrücken, 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:

Zusätzlich gibt es noch einige vordefinierte Objekte für die Fehlerbehandlung ( Error ). 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 area = 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, da man früher das Zahlensystem mit einem Präfix markierte. "0" stand für eine Oktalzahl, "0x" für eine Hexadezimalzahl.

parseInt("010");  //  8
parseInt("0x10"); // 16

Die Notation für Hexadezimahlzahlen ist auch noch im aktuellen Standard enthalten.

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
+ "010";  // 10
+ "0x10"; // 16

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 Rechenoperation 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 Unicode-Zeichen. Falls Sie schon einmal mit Internationalisierung zutun hatten, werden Sie darüber sicher erfreut sein. Genauer gesagt sind Strings Folgen von UTF-16 Codeeinheiten und jede Codeeinheit ist durch eine 16-Bit Zahl repräsentiert. Jedes Unicode-Zeichen besteht aus einer oder zwei dieser Codeeinheiten.

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 Anzahl der Zeichen eines Strings herauszufinden, greift man auf die Eigenschaft length zu:

"hello".length; // 5

Das war gerade unser erster Kontakt mit JavaScript Objekten! Haben wir schon erwähnt, dass man auch Strings wie Objekte verwenden kann? Sie haben auch Methoden, mit denen man den Strings verändern und bestimmte Informationen extrahieren kann.

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 einen Wert handelt, der einen "nicht Wert" repräsentiert und undefined, wobei es sich um einen Wert vom Typ 'undefined' handelt, welcher 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 aber noch einmal zur Verdeutlichung: Bei JavaScript ist es möglich, eine Variable zu deklarieren, ohne sie zu initialisieren, ihr also einen Wert zuzuweisen - 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, leere Strings (""), NaN , null , und undefined werden 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 kaum nö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. Mit der ECMAScript Edition 6 ist es mithilfe der neuen Schlüsselwörter let und const möglich, Variablen zu erstellen, die an den jeweiligen Block gebunden sind.

Operatoren

Die numerischen Operatoren sind +, -, *, / und % (nicht dasselbe wie Modulo). Werte werden mit dem Gleichzeichen = zugewiesen. Außerdem gibt es noch kombinierte Zuweisungsoperatoren 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ädekrement bzw. -inkrement unterschieden.

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

"hello" + " world"; // "hello world"

Addiert man einen String mit einer Zahl  (oder einem anderen Wert), wird alles zuerst zu einem String konvertiert. Dies bereitet manchmal Probleme:

"3" + 4 + 5;  // "345"
 3 + 4 + "5"; // "75"

Das Hinzufügen eines leeren Strings zu einem beliebigen Wert ist eine hilfreiche Methode, den Wert in einen String umzuwandeln.

Vergleiche können bei JavaScript mit den Operatoren <, >, <= und >= vorgenommen werden. Diese Operatoren funktionieren sowohl bei Strings als auch bei Zahlen. 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:

123 == "123"; // true
1 == true; // true

Damit unterschiedliche Datentypen berücksichtigt werden, muss man den ===-Operator verwenden:

123 === "123"; // false
1 === true;    // false

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

Außerdem gibt es bei JavaScript noch bitweise 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 Schlüsselwö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. Sie unterscheiden sich in der Anzahl der Iterationen: Die  do-while  Schleife wird immer mindestens einmal ausgeführt, die while-Schleife kann auch nie ausgeführt werden. Bei der while-Schleife wird die Abbruchbedingung also schon vor dem ersten Schleifendurchgang überprüft.

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++) {
  // Wird 5-Mal ausgeführt
}

Die Operatoren && und || arbeiten nach der Kurzschlussauswertung - 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 Vergleichsausdrü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 Hashtabellen 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.

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

Folgendes Beispiel erstellt einen Prototyp (Person) und eine Instanz dieses Prototyps (You).

function Person(name, age) {
  this.name = name;
  this.age = age;
}

// Definiere ein Objekt
var You = new Person("You", 24); 
// Wir erstellen eine Person mit dem Namen "You" und dem Alter 24

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"; // funktioniert

Ab ECMAScript 5 können reservierte Wörter bei Objektliteralen auch ohne Hochkommata verwendet werden. Siehe Spezifikation.

Weitere Informationen zu Objekten und Prototypen unter  Object.prototype.

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

Ein praktischerer Weg ist die Erstellung über ein Array-Literal.

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

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]
}

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. Deshalb wird nicht empfohlen, diese Methode zu verwenden.

Mit ECMAScript5 wurde mit forEach() noch ein Weg eingeführt, über ein Array zu iterieren:

["dog", "cat", "hen"].forEach(function(currentValue, index, array) {
  // Verarbeite currentValue oder array[index]
});

Das Hinzufügen eines Elements zu einem Array funktioniert folgendermaßen:

a.push(item);

Arrays besitzen verschiedene Methoden. Hier findet man die Dokumentation für alle Array Methoden.

Methodenname Beschreibung
a.toString() Gibt einen String zurück, der aus dem Ergebnis von toString() eines jeden Elements, mit Komma getrennt, besteht.
a.toLocaleString() Wie toString() , nur wird anstelle von toString() toLocaleString() aufgerufen. 
a.concat(item1[, item2[, ...[, itemN]]]) Erstellt eine Kopie von a, fügt dieser die übergebenen Elemente hinzu und gibt diese Kopie zurück.
a.join(sep) Wandelt das Array in einen String um. Die Elemente werden durch sep getrennt.
a.pop() Entfernt das letzte Element und gibt es zurück.
a.push(item1, ..., itemN) Push fügt eines oder mehrere Elemente am Ende hinzu.
a.reverse() Kehrt die Reihenfolge des Arrays um.
a.shift() Entfernt das erste Element und gibt es zurück.
a.slice(start, end) Gibt einen Teil eines Arrays zurück.
a.sort([cmpfn]) Sortiert das Array mithilfe der übergebenen Sortierfunktion cmpfn.
a.splice(start, delcount[, item1[, ...[, itemN]]]) Modifiziert ein Array, indem ein Teil gelöscht und durch mehrere Elemente ersetzt wird.
a.unshift(item1[, item2[, ...[, itemN]]]) Fügt Elemente am Anfang hinzu.

Funktionen

Neben Objekten, gehören auch Funktionen zu den Kernkomponenten von JavaScript. Die Syntax für eine einfache Funktion 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 beliebiger 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 Parameter übergeben werden:

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

Auf den ersten Blick mag dies 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 Kommata 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 dieser 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? JavaScript erlaub hierfür die Benennung von anonymen Funktionen. Dies nennt man "named IIFEs" ( Immediately Invoked Function Expressions).

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 des Sichtbarkeitsbereichs der Funktion verfügbar. Diese Vorgehensweise erlaubt der JavaScript-Engine den Code besser zu optimieren und auch die Lesbarkeit des Codes wird so verbessert.

Beachte, dass JavaScript-Funktionen ebenfalls Objete sind - wie alles andere in JavaScript - und man kann Attribute ändern und hinzufügen, wie wir es im Objekte-Abschnitt gesehen haben.

Benutzerdefinierte Objekte

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 Fehlerquelle. 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.

Das Problem, fullName() ohne Objekt aufzurufen, existiert jedoch immer noch.

Unser Objekt person reift so langsam heran, weist jedoch noch immer einige Ungereimtheiten auf: Bei jeder Erstellung eines person-Objekts werden jeweils auch immer zwei neue Funktionen 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 Prototyp-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 Prototyp 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 firstNameCaps() {
  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 reversed() {
  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 Prototyp Teil einer Suchkette. Der Ursprung dieser Kette ist Object.prototype. Dieser Prototyp 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.toString(); // "<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 Implementierung von 'new' dar:

function trivialNew(constructor, ...args) {
  var o = {}; // Create an object
  constructor.apply(o, args);
  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. In diesem Codeteil nennt man ...args (mit den Punkten) die sogenannten Restargumente. Dies enthält also alle weiteren übergebenen Argumente.

Der Aufruf von

var bill = trivialNew(Person, "William", "Orange");

ist somit fast äquivalent zu

var bill = new Person("William", "Orange");

Eine mit apply() verwandte Funktion ist die Funktion call, 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);
// Das gleiche wie
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 gleichzeitig zu den Konzepten, die sehr oft missverstanden werden. Was macht dieses Codebeispiel?

function makeAdder(a) {
  return function(b) {
    return a + b;
  };
}
var x = makeAdder(5);
var 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.

Das Verhalten 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. Hier sind einige gute Einführungen zu Closures zu finden.

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

 Mitwirkende an dieser Seite: Coke_and_Pepsi, schlagi123, ibafluss, creitiv, fscholz, eminor
 Zuletzt aktualisiert von: Coke_and_Pepsi,