JavaScript-Tutorium:Grundlagen
Ziel
Ziel dieses Tutoriums ist es die Grundlagen und Besonderheiten der Sprache JavaScript zu vermitteln. Zur Veranschaulichung sind viele Code-Beispiele gegeben.
Überblick
Geschichtliches
- Erfunden von Brendan Eich bei Netscape
- Adaptiert von Microsoft und JScript benannt
- Netscape und Microsoft entwickeln unterschiedliche Implementierung
- Überreicht von Netscape an ECMA und standardisiert als ECMAScript
Spracheigenschaften
- Syntax ist an C angelehnt
- Hat fast nichts zu tun mit Java
- Ist interpretiert, nicht kompiliert
- Looses Typsystem
- Objektorientiert, nicht klassenorientiert
- Begünstigt funktionales Programmieren
- Läuft in vielen unterschiedlichen Umgebungen
Umgebungen
- Browser, z.B. Firefox, Chrome, Internet Explorer, Safari, Opera
- Server, z.B. NodeJS, Windows Scripting Host, Rhino
- Applikationen, z.B. Adobe Photoshop, Quartz Composer
- Betriebssysteme, z.B. GNOME
JavaScript-Engines
- SpiderMonkey/JägerMonkey: Firefox, GNOME
- V8: Chrome, NodeJS
- Trident/Chakra: Internet Explorer
ECMAScript Versionen
- ES3 ist vollständig implementiert von allen gängigen Browsern
- ES5 ist vollständig implementiert von IE9+, FF4+, Chrome7+
- ES.next/Harmony ist von keinem Browser vollständig implementiert
Literaturempfehlungen
- "JavaScript: The Good Parts" - Douglas Crockford
- Mozilla Developer Network
Schlüsselwörter
Folgende Wörter sind für die Sprache selbst reserviert und können nicht als Variablennamen verwendet werden:
abstract
boolean break byte
case catch char class const continue
debugger default delete do double
else enum export extends
false final finally float for function
goto
if implements import in instanceof int interface
long
native new null
package private protected public
return
short static super switch synchronized
this throw throws transient true try typeof
var volatile void
while with
Die Auswahl der Begriffe orientiert sich stark an der Sprache Java.
Einige der Begriffe finden in JavaScript aktuell keine Verwendung.
Variablen
In JavaScript wird das Schlüsselwort var
verwendet um Variablen zu deklarieren (und zu initialisieren).
Definition und Zuweisung
Variablen können unmittelbar bei ihrer Deklaration definiert werden.
var spieler1 = 'John';
console.log(spieler1);
Oder auch zu späteren Zeitpunkten.
var aktuellePunktzahl;
aktuellePunktzahl = 2000;
console.log(aktuellePunktzahl);
Mehrere Variablen können mit Kommas separiert auf einmal deklariert werden.
var spieler1 = 'John', spieler2 = 'Jack';
console.log(spieler1);
console.log(spieler2);
Eine Typabgabe ist nicht notwendig, da es sich nicht um eine stark typisierte Sprachen handelt.
Die Engine kümmert sich automatisch um die Anforderung und Freigabe von Speicher.
var istSpielGewonnen = 1;
istSpielGewonnen = true;
console.log(istSpielGewonnen);
Wie auch in anderen Sprachen sind Mehrfachzuweisungen erlaubt.
var gegner1, gegner2, gegner3;
gegner1 = gegner2 = gegner3 = 'Monster';
console.log(gegner1);
console.log(gegner2);
console.log(gegner3);
Eine erneute Deklaration einer bereits bestehenden Variable führt zu keinem Fehler, sollte allerdings vermieden werden:
var istSpielGewonnen = false;
var istSpielGewonnen = true;
console.log(istSpielGewonnen);
Hoisting
Hoisting bedeutet dass der JavaScript-Interpreter alle Variablendefinitionen vor der Ausführung des Codes ()innerhalb der umgebenden Funktion) nach oben schiebt. Dies betrifft nur die Definitionen, nicht aber die initiale Belegung mit einem Wert.
Augenscheinlich kann man also Code schreiben, der Variablen verwendet bevor sie definiert sind.
Da jedoch die initiale Wertzuweisung nicht vom Interpreter verschoben wird kann dies zu unerwarteten Resultaten führen.
console.log(istSpielGewonnen);
var istSpielGewonnen = 5;
Tipp: Um Hoisting zu vermeiden ist es empfehlenswert alle Variablendeklaration immer am Anfang der
jewilig umgebenden Funktion zu schreiben.
Handelt es sich um globale Variable sollte diese am Anfang der JavaScript-Datei stehen.
Globale Variablen
Jede JavaScript-Umgebung besitzt ein globales Objekt, welches alle globalen Properties beherbergt.
Im Browser wird das globale Objekt über die Variable window
verfügbar gemacht. In Node.JS heisst es global
.
Falls eine Variable nicht innerhalb einer Funktion definiert wird, wird diese automatisch an das globale Objekt angehängt.
var spieler = 'John';
console.log('spieler global?', spieler === window.spieler);
var erstellePunktzahl = function() {
var punktzahl = 2000;
console.log('punktzahl global?', punktzahl === window.punktzahl);
}
erstellePunktzahl();
Achtung: Wird das Schlüsselwort var
vergessen, wird die Variable implizit als globale Variable definiert.
var erstellePunktzahl = function() {
punktzahl = 2000;
}
erstellePunktzahl();
console.log(punktzahl);
console.log(window.punktzahl);
Primitive Datentypen
JavaScript stellt folgende primitive Datentypen zur Verfügung:
Null, Undefined, Number, Boolean, String
Anmerkung: Number, Boolean und String werden auch als Objekte behandelt mittels des sogenannten Boxings.
Null
Null ist ein Datentyp, welcher nur einen definierten Wert annehmen kann: null
.
Im Gegensatz zu anderen Sprachen ist null
nicht der Standardwert einer nicht initialisierten Variable.
var spieler = null;
console.log(spieler);
Undefined
Undefined ist ein Datentyp, welcher ebenso nur einen definerten Wert annehmen kann: undefined
.
Dieser Wert ist der Standardwert einer nicht initialisierten Variable.
Er ist nicht gleichbedeutend mit null
.
var spieler1, spieler2 = null;
console.log(spieler1);
console.log(spieler1 === spieler2);
Boolean
Boolean ist ein boolscher Datentyp, welcher die Werte true
und false
annehmen kann.
var istSpielGewonnen = true, istSpielVerloren = false;
console.log(istSpielGewonnen);
console.log(istSpielVerloren);
console.log(istSpielGewonnen === istSpielVerloren);
Number
Number ist eine 64 bit breite Fließkommazahl, ähnlich zu double
wie in Java.
var punktzahl = 2000, pi = 3.14;
console.log(punktzahl);
console.log(pi);
JavaScript besitzt keinen separaten Datentyp für Integer-Zahlen.
Integer-Werte werden auch als Number abgebildet.
var punktzahl1 = 100, punktzahl2 = 100.00;
console.log(punktzahl1 === punktzahl2);
Es existieren die gängigen arithmetischen Operatoren für Berechnungen.
console.log(1 + 2);
console.log(1 - 2);
console.log(1 * 2);
console.log(1 / 2);
console.log(--1);
console.log(++1);
Anmerkung:
Da es sich bei jeder Zahl um eine Fließkommazahl handelt gibt es keine ganzzahlige Teilung.
Diese muss über einen Umweg mithilfe der Number API emuliert werden.
var quotient = Math.floor(5 / 3);
console.log(quotient);
Der Wert NaN (not a number) ist das Ergebnis einer mathematischen Operation die nicht in einer Zahl resultiert.
var ungueltigeOperation = 100 / 'a';
console.log(ungueltigeOperation);
Zahlen vom Typ Number können auch in oktaler und hexadezimaler Form geschrieben werden.
var oktal = 017, hexadezimal = 0x10;
console.log(oktal);
console.log(hexadezimal);
Number API
Der Datentyp Number stellt diverse Hilfsfunktionen zur Verfügung.
console.log(Number.MAX_VALUE);
console.log(Number.MIN_VALUE);
var zahl = 2;
console.log(zahl.toFixed(3));
Eine komplette Referenz der Number API finden sie auf
dieser MDN-Seite.
String
Der Datentyp String wird verwendet um unveränderliche Folgen von 16 bit breiten Zeichen darzustellen. Anders als in anderen Sprachen existiert kein Datentyp für einzelne Zeichen.
Strings können sowohl von einfachen als auch von doppelten Anführungszeichen umgeben sein.
var spieler1 = 'Paul', spieler2 = "Peter";
console.log(spieler1);
console.log(spieler2);
console.log(spieler1 === spieler2);
Um Anführungszeichen in Strings zu verwenden müssen diese mit vorangestellten Backslash versehen werden.
console.log('\'');
console.log("\"");
Strings können mithilfe des Plus-Operators zu einem neuen String zusammengesetzt werden.
console.log('John' + ' ' + 'Doe');
String API
Ebenso wie der Datentyp Number stellt String unterschiedliche Hilfsfunktionen zur Verfügung.
var spielerName = 'John Doe';
console.log('String-Länge: ' + spielerName.length);
console.log('Als Großbuchstaben: ' + spielerName.toUpperCase());
console.log('Als Kleinbuchstaben: ' + spielerName.toLowerCase());
console.log('Position vom ersten "e": ' + spielerName.indexOf('e'));
console.log('Zeichen an Stelle 4: ' + spielerName.charAt(3));
Eine komplette Referenz der String API finden sie auf
dieser MDN-Seite.
Vergleiche
Gleichheit (==)
Der Gleichheitsoperator konvertiert Datentypen der beiden zu vergleichenden Ausdrücke und vergleicht dann die Werte. Der Ungleichheitsoperator verhält sich analog.
Bei Typungleicheit werden folgenden Regeln angewendet:
1.null
undundefined
sind gleich 2. Strings werden in Zahlen konvertiert 3. Boolsche Werte werden in Zahlen konvertiert 4. Objekte werden konvertiert mittles valueOf() wenn Sie mit Number vergleichen werden 5. Objekte werden konvertiert mittels toString() wenn Sie mit String vergleichen werden
console.log(null == undefined);
console.log('3' == 3);
console.log(true == 1);
console.log(new Object() == '[object Object]');
Anmerkung: Diese Regeln gelten für die Typumwandlung.
Es ist ohne weiteres möglich den Gleichheitsoperator sinnvoll zu verwenden um z.B. Objektreferenzen zu vergleichen.
var spieler1 = {}, spieler1 = {};
console.log(spieler1 == spieler1);
console.log(spieler1 == spieler2);
Identität
Der Identitätsoperator vergleicht zuerst die Datentypen und bei Typgleichheit die Werte zweier Ausdrücke.
console.log(null === undefined);
console.log('3' === 3);
console.log(true === 1);
console.log(new Object() === '[object Object]');
Anmerkung: Die Verwendung des Gleichheitsoperators wird in großen Teilen der JavaScript-Gemeinde als
schlechte Programmierpraxis angesehen. Oftmals wird ausschließlich der Identitätsoperator verwendet.
Größer und kleiner
Die Vergleichsoperatoren größer (gleich) und kleiner (gleich) arbeiten auf die gleiche Weise wie der Gleichheitsoperator.
console.log(2 > '1');
console.log(true > 1);
Kontrollstrukturen
if-Anweisung
Die Syntax der if-Anweisung ist identisch zu den meisten anderen Sprachen.
Es gilt aber zu beachten, dass folgende Ausdrücke als false
interpretiert werden:
false, null, undefined, '', 0, NaN
if (!false) {
console.log('!false ergibt true');
}
if (!null) {
console.log('!null ergibt true');
console.log('null gleich false: ' + (null == false));
}
if (!undefined) {
console.log('!undefined ergibt true');
console.log('undefined gleich false: ' + (undefined == false));
}
if (!'') console.log('!"" ergibt true');
if (!NaN) console.log('!NaN ergibt true');
Boolsche Logik
Die Boolsche Logik funktioniert ebenfalls wie in anderen Sprachen, inklusive Short-Circuit.
if (false || null || undefined || '' || NaN || true) {
console.log(false || null || undefined || "" || NaN || true);
}
var nichtVeraenderteVariable = 1;
if (false && nichtVeraenderteVariable++) {
}
console.log(nichtVeraenderteVariable);
Der ternäre Operator interpretiert die gleichen Werte wie das if-Statement als false:
console.log(true ? 1 : 0);
console.log(null ? 1 : 0);
console.log(null != false ? 1 : 0);
switch-Anweisung
Die switch
-Anweisung vergleicht einen gegebenen Ausdruck mit einer Liste von Werten (per Identität).
Stimmt die Identität eines Wertes mit dem Ausdruck überein wird der nachfolgende Code ausgeführt.
Eine break
-Anweisung beendet in solch einem Fall die Code-Ausführung frühzeitig.
var level = 3;
switch (level) {
case 1:
console.log('Level 1');
break;
case 2:
console.log('Level 2');
break;
default:
console.log('0');
break;
}
while- und do-while-Schleifen
Die while
-Schleife führt ein Stück Code so lange wie aus wie ein gegebener Ausdruck den Wert true
ergibt.
Ergibt der Ausdruck beim ersten Mal bereits false
so wird der gegebene Code nie ausgeführt.
var countdown = 10;
while (countdown--) {
console.log(countdown);
}
while (false) {
console.log('wird nicht ausgegeben');
}
Die do
-while
-Schleife funktioniert wie die while
-Schleife mit der Ausnahme,
dass der gegebene Schleifenkörper auf jeden Fall einmal ausgeführt wird.
do {
console.log('wird ausgegeben');
} while (false);
try, catch und throw
Die Anweisungen try
und catch
werden verwendet um Exceptions abzufangen und zu behandeln.
try {
console.log(objekt.eigenschaft.eigenschaft);
}
catch (error) {
console.log(error);
}
throw
wird verwendet um Exceptions zu werfen. Als Argument sollte ein Error
-Objekt übergeben werden.
try {
throw new Error('Custom error');
}
catch (error) {
console.log(error);
}
for- und for-in-Schleifen
for
-Schleifen funktionieren ebenfalls wie in den meisten anderen Sprachen.
Der for
-in
-Loop wird verwendet um über Eigenschaften eines Objekts zu iterieren.
var spieler = {name: 'Max Mustermann', leben: 3, punkte: 2000};
for (var eigenschaft in spieler) {
console.log(eigenschaft);
}
Anmerkung: for..in
sollte in der Regel mit hasOwnProperty()
verwendet werden.
Objekte
Jeder Wert mit einem anderen Datentypen als ein primitiver ist ein Objekt. Objekte sind veränderliche Sammlungen von Eigenschaften.
Eine Eigenschaft ist ein Schlüssel-Wert-Paar mit einem String als Schlüssel und einem beliebigen Typ als Wert.
Objekte sind vergleichbar mit Hash-Maps und/oder Dictionaries aus anderen Sprachen.
JavaScript stellt native Objekttypen wie Funktionen, Arrays, reguläre Ausdrücke, Daten und generische Objekte bereit.
Anmerkung: Variablen speichern immer Referenzen auf Objekte, nicht die Objekte selbst.
Erstellung von Objekten
Die einfachste Art und Weise ein Objekt zu erstellen ist mithilfe des Objektliterals {}
.
var spieler1 = {};
console.log(spieler1);
Eine weitere Möglichkeit ist der Objektkonstruktor, welche allerdings keine Vorteile gegenüber dem Literal bietet.
var spieler1 = new Object();
console.log(spieler1);
Objekte können bei ihrer Erstellung mit Eigenschaften und Werten vorbelegt werden.
Die Eigenschaften werden als kommaseparierte Liste angegeben.
Eigenschaft und Wert werden jeweils durch einen Doppelpunkt getrennt.
var spieler1 = {name: 'John Doe', nickname: 'JD', punktzahl: 0};
console.log(spieler1);
Eigenschaften müssen in Anführungszeichen angegeben werden falls es sich um reservierte Wörter handelt
oder der Name der Eigenschaft Zeichen enthält, welche nicht bei Variablennamen vorkommen dürfen.
var bier = {'export': true, 'ist alkoholfrei': false};
console.log(bier);
Eigenschaften können sowohl mit Werten primitiver Datentypen als auch mit Objekten und Funktionen belegt werden.
var spieler1 = {
name: 'John Doe',
punktzahl: 0,
springe: function() {
console.log('Spieler springt');
},
};
console.log(spieler1);
spieler1.springe();
Lesen von Eigenschaften
Es gibt verschiedene Arten um auf die Eigenschaften eines Objekts zuzugreifen.
Zum einen kann die Punktnotation verwendet werden:
var spieler1 = {name: 'Max Mustermann'};
console.log(spieler1.name);
Zum anderen ist es möglich einen String-Ausdruck in eckigen Klammern anzugeben:
var spieler1 = {name: 'Max Mustermann'};
console.log(spieler1['name']);
Normalerweise wird die erste Variante verwendet, wenn der Name der Eigenschaft bekannt und statisch ist.
Die zweite Variante wird vor allem verwendet wenn der Eigenschaftsname dynamisch zur Laufzeit bestimmt wird.
var spieler1 = {name: 'Max'};
var hatObjektEigenschaft = function(objekt, eigenschaft) {
return objekt[eigenschaft] ? true : false;
}
console.log(hatObjektEigenschaft(spieler1, 'name');
console.log(hatObjektEigenschaft(spieler1, 'nickname');
Ebenso wie bei der initialen Befüllung muss auch ein String-Ausdruck verwendet werden wenn es sich bei dem Namen
der Eigenschaft um ein reserviertes Schlüsselwort handelt oder aber unerlaubte Zeichen für Variablennamen enthalten sind.
var spieler1 = {'hat highscore erreicht': true};
console.log(spieler1['hat highscore erreicht']);
Ein Zugriff auf eine nicht definierte Eigenschaft eines Objekts liefert den Wert undefined
zurück.
var spieler1 = {};
console.log(spieler1.name);
Schreiben von Werten
Analog zum Lesen können Werte von Eigenschaften sowohl durch Punktnotation als auch String-Ausdruck verändert werden.
var spieler1 = {punktzahl: 5};
spieler1.punktzahl += 5;
spieler1['punktzahl'] += 5;
console.log(spieler1.punktzahl);
Wichtig: Beim schreibenden Zugriff auf eine nicht existente Eigenschaft eines Objekts wird diese implizit definiert.
Das bedeutet, dass jedem Objekt dynamisch zur Laufzeit jederzeit beliebige Eigenschaften zugewiesen werden können.
var spieler1 = {};
spieler1.name = 'Rainer Zufall';
spieler1.punktzahl = 42;
spieler1.verwendetGamepad = true;
console.log(spieler1);
Untersuchen von Eigenschaften
Um alle Eigenschaften eines Objekts abzufragen wird der for in
-Loop verwendet.
var spieler1 = {name: 'Max Mustermann', punktzahl: 42, verwendetGamepad: true};
for (var eigenschaft in spieler1) {
if (spieler1.hasOwnProperty(eigenschaft)) {
console.log(eigenschaft + ': ' + spieler1[eigenschaft]);
}
}
Die Funktion hasOwnProperty()
verhindert dabei, dass vererbte Eigenschaften ausgegeben werden.
Löschen von Eigenschaften
Eigenschaften können mittels dem delete
Operator von Objekten entfernt werden.
var person1 = {hatBrille: true};
delete person1.hatBrille;
console.log(person1);
Anmerkung: Den Wert einer Eigenschaft auf undefined
zu setzen ist nicht mit einem delete
gleichzusetzen.
Die Zuweisung von undefined
lässt die Eigenschaft auf dem Objekt bestehen, z.B. relevant für for in
-Schleifen.
Arrays
In JavaScript sind Arrays genau genommen Objekte mit listenähnlichem Verhalten. Im Gegensatz zu anderen Sprachen besitzen sie eine dynamische Länge.
Erstellung
Analog zur Objekterstellung ist der einfachste Weg ein Array zu erstellen mittels des Literals []
.
var spielerListe = [];
console.log(spielerListe);
Alternativ kann man ebenfalls einen Konstruktor verwenden und die length
Eigenschaft initial definieren.
var spielerListe = new Array(10);
console.log(spielerListe);
console.log(spielerListe.length);
Arrays können ebenso initial mit Werten vorbefüllt werden.
var spielerListe = [{}, {}, {}, {}, {}];
console.log(spielerListe);
Da JavaScript generell nicht stark typisiert ist können Arrays Werte unterschiedlicher Typen enthalten.
var spielerListe = [{name: 'Max Mustermann'}, {vorname: 'John', nachname: 'Doe'}];
console.log(spielerListe);
Die bereits erwähnte Eigenschaft length
beschreibt die aktuelle Anzahl an Werten im Array.
console.log('array has a length of ' + [1, 2, 3, 4, 5].length);
Multidimensionale Arrays können ebenfalls mit Literalen erzeugt werden.
var ticTacToeSpielfeld = [
['O', 'O', 'X'],
['O', 'X', 'X'],
['O', 'X', 'O']
];
Lesen von Werten
Werte von Arrays können über den numerischen Index abgefragt werden.
var spielerListe = [{name: 'Max Mustermann'}, {name: 'John Doe'}];
console.log(spielerListe[0]);
Schreiben von Werten
Ebenso können Werte direkt über den Zugriff auf den Index verändert werden.
var spielerListe = [{vorname: 'Mäxle', nachname: 'Mustermann'}];
spielerListe[0].vorname = 'Max';
console.log(spielerListe);
Wird auf einen noch nicht belegten Index zugegriffen, wird das Array entsprechend erweitert.
Bisher nicht belegte Stellen werden mit dem Wert undefined
belegt.
var spielerListe = [{name: 'Max Mustermann'}, {name: 'John Doe'}];
console.log(spielerListe);
console.log(spielerListe.length);
spielerListe[5] = {name: 'Frau Holle'};
console.log(spielerListe);
console.log(spielerListe.length);
Veränderungen der Eigenschaft length
legen die aktuelle Größe eines Arrays fest unabhängig vom Inhalt.
var spielerListe = [];
spielerListe.length = 10;
console.log(spielerListe);
Um Werte ans Ende des Arrays einzufügen wird die push()
-Funktion verwendet.
var spielerListe = [];
var spieler1 = {name: 'Max Mustermann'};
spielerListe.push(spieler1);
console.log(spielerListe);
Die pop()
-Funktion entfernt hingegen das aktuell letzte Element eines Arrays und verringert dessen Größe um 1.
var spielerListe = [{name: 'Max Mustermann'}];
var spieler1 = personenregister.pop();
console.log(spielerListe);
console.log(spieler1);
Die Funktionen unshift()
und shift()
funktionieren wie push()
und pop()
, jedoch für den Anfang eines Arrays.
var spielerListe = [];
var spieler1 = {name: 'Max Mustermann'};
spielerListe.unshift(spieler1);
console.log(spielerListe);
personenregister.shift();
console.log(spielerListe);
Um mehrere Arrays miteinander zu kombinieren wird die concat()
-Funktion verwendet.
console.log([1, 2].concat([3, 4]));
Eine komplette Referenz der Array API ist auf
dieser
MDN-Seite zu finden.
Enumerierung
Um alle Elemente eines Arrays abzufragen wird der for
-Loop verwendet.
var spielerListe = [{name: 'Max Moritz'}, {name: 'John Doe'}];
for (var index = 0; index < spielerListe.length; index++) {
console.log(index + ': ' + spielerListe[index]);
}
Funktionen
Funktionen sind Objekte in JavaScript. Das bedeutet sie haben Eigenschaften und können Variablen zugewiesen werden.
Eine Funktion besteht aus dem Schlüsselwort function
, einem Namen, einer Liste von Argumenten und einem Funktionskörper.
Ausführung und Erstellung
Funktionen werden ausgeführt indem man sie mit ihrem Namen aufruft und eine Liste an Argumenten übergibt.
console.log('Hello World');
var maximum = Math.max(42, 23);
console.log(maximum);
Es gibt zwei Möglichkeiten Funktionen zu erstellen.
Zum einen kann eine sogenannte Funktionsdeklaration verwendet werden.
Diese führt dazu, dass eine Variable mit demselben Namen der Funktion definiert wird, welche auf die Funktion zeigt.
function starteSpiel() {
console.log('Spiel gestartet!');
}
starteSpiel();
Funktionsdeklaration unterliegen dem sogenannten Hoisting.
Hoisting bedeutet, dass der JavaScript-Interpreter die Funktionsdeklaration vor der
eigentlichen Ausführung des Codes innerhalb der umgebenden Funktion nach oben an den Anfang schiebt.
Das heisst man kann Code schreiben in dem eine Funktion augenscheinlich aufgerufen wird bevor sie definiert ist.
starteSpiel();
function starteSpiel() {
console.log('Spiel gestartet!');
}
Die zweite Möglichkeit eine Funktion zu erstellen besteht darin einen Funktionsausdruck einer Variable zuzuweisen.
Der Funktionsausdruck sieht dabei der Deklaration sehr ähnlich, außer dass in der Regel kein Funktionsname vergeben wird.
var starteSpiel = function() {
console.log('Spiel gestartet!');
};
starteSpiel();
Funktionsausdrücke werden besonders dann bevorzugt wenn man die negativen Effekte des Hoisting minimieren möchte.
starteSpiel(); // verursacht Fehler
var starteSpiel = function() {
console.log('Spiel gestartet!');
};
Empfehlung: Verwenden Sie immer Funktionsausdrücke, welche Variablen zugewiesen werden.
(Anonyme) Funktionsausdrücke sind aufgrund ihrer kompakten Schreibweise auch nützlich wenn es darum geht Funktionen als Argumente oder Rückgabewerte zu verwenden.
Argumente
Funktionsargumente werden definiert als kommaseparierte Liste und werden bei der Ausführung auch als solche übergeben.
var erstelleSpielerNamen = function(vorname, nachname) {
return vorname + ' ' + nachname;
}
var spielerName = erstelleSpielerNamen('Max', 'Mustermann');
console.log(spielerName);
Jede Funktion in JavaScript kann mit einer beliebigen Anzahl an Argumenten aufgerufen werden.
var erstelleSpielerNamen = function(vorname, nachname) {
return vorname + ' ' + nachname;
}
console.log(kombiniere());
console.log(kombiniere('Max'));
console.log(kombiniere('Max', 'Mustermann'));
Dabei werden nicht belegte Argumente mit dem Wert undefined
initialisiert.
Funktionen verfügen über eine lokale Variable namens arguments
.
Diese ist ein Array-artiges Objekt und enthält alle Argumente die beim Aufruf übergeben wurden.
var zaehleArgumente = function() {
return arguments.length;
}
console.log(zaehleArgumente(1));
console.log(zaehleArgumente(1, 2, 3));
console.log(zaehleArgumente(true, null, undefined));
Außerdem besitzen Funktionen eine Eigenschaft namens length
welche die Anzahl an erwarteten Argumenten beschreibt.
var erstelleSpielerNamen = function(vorname, nachname) {}
console.log(erstelleSpielerNamen.length);
Rückgabewert
Funktionen in JavaScript können beliebige Werte zurückgeben.
Ist kein expliziter Rückgabewert vorhanden gibt eine Funktion implizit undefined
zurück.
var gibNichtsZurück = function() {
}
console.log(gibNichtsZurück());
var gibEinsZurück = function() {
return 1;
}
console.log(gibEinsZurück());
Anmerkung: Ein impliziter Rückgabewert von undefined
ist nicht gegeben wenn eine Funktion mit new
aufgerufen wird.
Es kann sogar zur Laufzeit entschieden werden ob ein Wert zurückgegeben wird.
var erstelleSpielerKuerzel = function(name) {
if (name) {
return name.charAt(0).toUpperCase();
}
}
console.log(erstelleSpielerKuerzel('Max'));
Scope
Der Scope kontrolliert die Sichtbarkeit und Lebensdauer von Variablen und Argumenten. Anders als in anderen Sprachen besitzt JavaScript einen funktionsbasierten Scope.
var nachricht = 'Hallo Spieler!';
var sprache = 'deutsch';
if (sprache == 'englisch') {
// Die bereits bestehende Variable wird überschrieben
var nachricht = 'Hello Gamer!';
}
console.log(nachricht);
Anmerkung: In neueren ECMAScript-Versionen ist es möglich mithilfe des Schlüsselworts
let
Variablen mit Block-Scope zu deklarieren.
Innere Funktionen
Wie bereits erwähnt sind Funktionen Objekte und können auch Variablen zugewiesen werden. Deswegen kann eine Funktion in ihrem Funktionskörper auch weitere lokale Funktion erstellen.
var erstelleSpieler = function() {
var erstelleName = function() {
return 'Max';
};
var erstellePunktzahl = function() {
return 200;
};
return {
name: erstelleName(),
punktzahl: erstellePunktzahl()
};
console.log(erstelleSpieler());
Objektfunktionen
Funktionen können ebenso Variablen und somit auch Objekteigenschaften zugewiesen werden.
var spieler1 = {
name: 'Max',
springe: function() {
console.log('Spieler springt');
},
};
console.log(spieler1.springe);
spieler1.springe();
Selbst nach Erstellung eines Objekts können Funktion als Eigenschaften referenziert werden.
var addiere = function(a, b) {
return a + b;
};
var taschenrechner = {memory: 42, batterie: 0.8};
taschenrechner.addiere = addiere;
console.log(taschenrechner.addiere(taschenrechner.memory, 23));
Konstruktoren
JavaScript ist eine objektorientierte Sprache, bestitzt aber keine Klassen im Gegensatz zu klassienbasierten Sprachen. Eine Möglichkeit Objekte mit ähnlichem Verhalten und ähnlicher Struktur zu erstellen sind Konstruktorfunktionen.
Verwendung
Jenseits der normalen Ausführung kann jede Funktion mit dem Schlüsselwort new
aufgerufen werden.
var addiere = function(a, b) {
return a + b;
}
console.log(addiere(4, 3));
console.log(new addiere(4, 3));
Dies ergibt allerdings nur dann Sinn wenn die Funktion auch als Konstruktorfunktion gedacht ist.
Ebenso ist es nicht sinnvoll als Konstruktoren gedachte Funktionen ohne das Schlüsselwort new
aufzurufen.
Dies kann zu unerwarteten Seiteneffekten führen.
var Spieler = function() {
};
console.log(Spieler());
console.log(new Spieler());
Funktionen, welche als Konstruktoren dienen, sollten mit einem Großbuchstaben beginnen um sie deutlich zu kennzeichnen.
var spieler = function() {}; // Falsch
console.log(spieler()); // ?
console.log(new spieler()); // ?
var Spieler = function() {}; // Richtig
console.log(new Spieler()); // !
Wenn eine Funktion mit dem Schlüsselwort new
aufgerufen wird,
wird ein automatisch erzeugtes Objekt am Ende der Funktion zurückgegeben.
var Spieler = function() {
};
var spieler1 = new Spieler();
spieler1.name = 'John Doe';
console.log(spieler1);
this
Das Schlüsselwort this
zeigt innerhalb einer Konstruktorfunktion auf das automatisch erzeugte Objekt.
var Spieler = function(name) {
this.name = name;
};
var spieler1 = new Spieler('John');
console.log(spieler1);
Achtung:
Wird eine Konstruktorfunktion ohne new
aufgerufen so zeigt this
aber auf das globale Objekt (im Browser: window
).
var Spieler = function(name) {
this.name = name;
}
Spieler('John Doe');
console.log(window.name);
Erzwingung von new
Es ist bedingt möglich sicherzustellen, dass eine Funktion mit new
aufgerufen wurde.
Zum Beispiel kann überprüft werden ob this
auf das globale Objekt zeigt (window
).
var Spieler = function(name) {
if (this === window) {
throw new Error('Spieler is a constructor');
}
this.name = name;
}
console.log(new Spieler('John'));
console.log(Spieler('John'));
Factories
Konstruktoren sollten nur verwendet werden wenn die Funktionsweise klar ist. Aufgrund der Flexibilität von JavaScript gibt es auch andere Möglichkeiten gleichartige Objekte zu erzeugen.
var erstelleSpieler = function(name) {
return {name: name};
}
var spieler1 = erstelleSpieler('John');
console.log(spieler1);
this
Das Schlüsselwort this
hat in JavaScript unterschiedliche Bedeutungen je nach Kontext.
Des Weiteren kann es mithilfe von ein paar nativen Funktionen manipuliert werden.
Unterschiedliche Kontexte
1. Wenn eine Funktion über eine Variable aufgerufen wird und somit
nicht als Eigenschaft eines Objekts, dann zeigt this
auf das globale Objekt.
var loggeThis = function() {
console.log(this);
};
Dabei spielt es keine Rolle ob die Funktion global ist oder sich innerhalb einer anderen Funktion befindet.
var erstelleUndRufeLoggeThisAuf = function() {
var loggeThis = function() {
console.log(this);
};
loggeThis();
};
erstelleUndRufeLoggeThisAuf();
2. Wenn eine Funktion als Eigenschaft eines Objekts aufgerufen wird, dann zeigt this
auf das Objekt.
var spieler1 = {
name: 'John',
springe: function() {
console.log(this.name + ' springt');
}
};
console.log(spieler1.springe());
Wichtig ist zu beachten wie eine Funktion zum Zeitpunkt des Aufrufens referenziert wird.
Da Funktionen freie Objekte sind können sie gleichzeitig als Objekteigenschaften und auch Variablen verfügbar sein.
var springe = function() {
console.log(this.name + ' springt');
};
var spieler1 = {
name: 'John',
springe: springe
}
console.log(spieler1.springe());
console.log(springe());
3. Wenn eine Funktion als Konstruktor mit new
aufgerufen wird, dann zeigt this
auf das automatisch erstellte Objekt.
var Spieler = function(name) {
this.name = name;
this.springe = function() {
console.log(this.name + ' springt');
}
};
var spieler1 = new Spieler('John');
spieler1.springe();
Manipulation von this
Mithilfe von zwei Funktionen kann kontrolliert werden auf was this
innerhalb einer aufgerufenen Funktion verweist.
apply()
ist auf jeder Funktion verfügbar, führt bei Aufruf die Funktion selbst aus und akzeptiert zwei Argumente.
Das erste Argument ist das Objekt auf welches this
bei der Ausführung zeigt.
Das zweite Argument ist ein Array, welches als Liste von Argumenten für den Aufrufe verwendet wird.
var Spieler = function(name) {
this.name = name;
}
var springe = function(hoch) {
console.log(this.name + ' springt' + hoch ? ' hoch' : '');
}
var person1 = new Spieler('John');
springe.apply(person1, [true]);
Die Funktion call()
ist sehr ähnlich zu apply()
, akzeptiert allerdings beliebig viele Argumente.
Das erste Argument ist ebenfalls das Objekt, auf welches this
bei der Funktionsausführung zeigt.
Die restlichen Argumente sind wiederum die Argumente, welche an die ausführende Funktionen übergeben werden.
Empfehlung: In der Regel ist die Verwendung von apply()
flexibler, da sie mit einem Array arbeitet.
Im Normalfall ist eine Manipulation von this
nicht notwendig und kann auf unterschiedliche Arten umgangen werden.
var Spieler = function(name) {
this.name = name;
}
var springe = function(spieler, hoch) {
console.log(spieler.name + ' springt' + hoch ? ' hoch' : '');
}
var spieler1 = new Spieler('John');
springe(spieler1, true);
Sichere Verwendung in Konstruktorfunktionen
Einer der häufigsten Fehler in JavaScript ist die falsche Verwendung von this
innerhalb von Konstruktorfunktionen.
var Spieler = function(name) {
this.name = name;
this.springe = function() {
console.log(this.name + ' springt');
}
this.laufe = function() {
console.log(this.name + ' läuft');
};
}
var fuehreFunktionAus = function(funktion) {
console.log(funktion());
};
var spieler1 = new Spieler('John');
fuehreFunktionAus(spieler1.springe);
Das Problem kann durch eine Variable gelöst werden, welche eine Referenz auf this
vorhält.
var Spieler = function(name) {
var self = this;
self.name = name;
self.springe = function() {
console.log(self.name + ' springt');
}
self.laufe = function() {
console.log(self.name + ' läuft');
};
}
var fuehreFunktionAus = function(funktion) {
console.log(funktion());
};
var spieler1 = new Spieler('John');
fuehreFunktionAus(spieler1.springe);
Anmerkung: Die Hilfsvariable ist kein Feature von JavaScript und stellt somit einen "Hack" dar.
Prototypen
Anmerkung: Der folgende Abschnitt ist optional und für das Eigenstudium. Er ist nicht vorlesungs-/prüfungsrelevant.
Mithilfe von Prototypen ist es möglich Vererbungshierarchien abzubilden und Wiederverwendung zu erreichen.
In JavaScript spricht man nicht von Instanzen und Klassen, sondern von Objekten und Prototypen.
Jedes Objekt kann auf einen Prototypen verweisen, muss dies aber nicht. Prototypen selbst sind ebenfalls Objekte.
Zugriff auf Prototypen
Jede erstellte Funktion erhält automatisch eine Eigenschaft namens prototype
, welches auf den Prototypen verweist.
Dieser erhält außerdem eine Eigenschaft constructor
, welche wiederum auf die Konstruktorfunktion selbst verweist.
var Spieler = function() {};
var prototyp = Spieler.prototype;
console.log(prototyp.constructor == Spieler);
Jedes Mal wenn ein Objekt mit einem Konstruktor erzeugt wird, erhält es eine versteckte Referenz auf diesen Prototypen.
In älteren ECMAScript-Versionen gibt es keine standardisierte Weise um auf den Prototypen eines Objekts zuzugreifen.
In ECMAScript 5 wurde dafür die Funktion Object.getPrototypeOf()
eingeführt.
var Spieler = function() {};
var spieler = new Spieler();
console.log('Prototyp: ' + Object.getPrototypeOf(spieler));
console.log(Object.getPrototypeOf(spieler) == Spieler.prototype);
Prototypen erweitern
Da es sich beim Prototypen eines Konstruktors um eine Referenz auf ein Objekt handelt kann dies beliebig modifiziert werden.
var Spieler = function() {};
Spieler.prototype.erstelleKuerzel = function() {
return this.name.charAt(0);
};
var spieler = new Spieler();
console.log('Prototyp: ' + Object.getPrototypeOf(spieler));
Es ist auch möglich die Referenz selbst zu verändern. Eine solche Änderung betrifft aber nicht bereits erzeugte Objekte.
var Spieler = function() {};
Spieler.prototype = {
erstelleKuerzel: function() {
return this.name.charAt(0);
};
};
var spieler = new Spieler();
console.log('Prototyp: ' + Object.getPrototypeOf(spieler));
Spieler.prototype = {};
console.log(Object.getPrototypeOf(spieler) == Spieler.prototype);
Die Prototype-Chain
Beim Zugriff auf eine Eigenschaft eines Objekts wird zunächst überprüft ob das Objekt selbst diese Eigenschaft bestizt.
var Spieler = function(name) {
this.name = name;
};
var spieler = new Spieler('John');
console.log(spieler.name);
Ist dies nicht der Fall wird überprüft ob der Prototyp des Objekts diese Eigenschaft besitzt.
Besitzt der Prototyp eine passende Eigenschaft wird diese verwendet, so als ob sie zum Objekt selbst gehört.
var Spieler = function(name) {
this.name = name;
};
Spieler.prototype.erstelleKuerzel = function() {
return this.name.charAt(0);
};
var spieler = new Spieler();
console.log(spieler.erstelleKuerzel());
Besitzt der Prototyp selbst nicht die Eigenschaft wird getestet ob das wiederum bei seinem Prototypen der Fall ist.
Dies wird so lange fortgesetzt bis die Eigenschaft gefunden wird oder bis es keinen Prototypen mehr in der Kette gibt.
var Spieler = function(name) {
this.name = name;
};
prototypVomPrototyp = Object.getPrototypeOf(Spieler.prototype);
prototypVomPrototyp.erstelleKuerzel = function() {
return this.name.charAt(0);
};
var spieler = new Spieler();
console.log(spieler.erstelleKuerzel());
Anmerkung: Der Prototyp einer selbst erstellten Funktion ist ein automatisch erzeugtes Objekt.
Dementsprechend ist der Prototyp des Prototypen wiederum der der Object
-Konstruktorfunktion.
var Spieler = function() {};
var prototypVomPrototyp = Object.getPrototypeOf(Spieler.prototype);
console.log(prototypVomPrototyp == Object.prototype);
Wiederverwendung
Wenn man mit vielen Objekten arbeitet und auf den Speicherverbrauch achten muss können Prototypen helfen. Verwendet man einen Konstruktor, der jedem neuen Objekt eine Funktion zuweist, ist das jedes Mal eine neue Funktion.
var anzahlErstellterFunktionen = 0;
var erstelleHalloWeltFunktion = function() {
anzahlErstellterFunktionen++;
return function() {
console.log('Hallo Welt');
};
};
var Spieler = function() {
this.sagHallo = erstelleHalloWeltFunktion();
};
var spieler = [];
for (var i = 0; i < 1000; i++) {
spieler.push(new Spieler());
};
console.log('anzahlErstellterFunktionen ist ' + anzahlErstellterFunktionen);
Weist man jedoch anstelle der Objekte dem Prototypen eine Funktion zu so wird nur eine erzeugt und diese wiederverwendet.
var Spieler = function() {};
Spieler.prototype.sagHallo = function() {
console.log('Hallo Welt');
};
var spieler1 = new Spieler();
var spieler2 = new Spieler();
console.log(spieler1.sagHallo == spieler2.sagHallo);
console.log(spieler1.sagHallo == Spieler.prototype.sagHallo);
console.log(spieler2.sagHallo == Spieler.prototype.sagHallo);
Prototypische Vererbung
Wie bereits erwähnt ist es mit Prototypen möglich Wiederverwendung und ähnliches Verhalten von Objekten zu erreichen. Verwendet man ein Objekt als Prototypenm, welches auch mit einem Konstruktor erstellt wurde, erhält man prototypische Vererbung.
var Person = function() {
this.sagHallo = function() {
console.log('Hallo Welt');
};
};
var Spieler = function() {
this.springe = function() {
console.log('Spieler springt');
};
};
Spieler.prototype = new Person();
var spieler1 = new Spieler();
console.log(spieler1.sagHallo());
console.log(spieler1.springe());
Prototypen auf nativen Objekten
Jedes Objekt in JavaScript wird mithilfe einer Konstruktorfunktion erzeugt welche einen Prototypen besitzt. Demzufolge ist es möglich das Verhalten von nativen Objekten zu verändern indem man auf den Prototypen modifiziert.
Object.prototype.sagMirWasDuBist = function() {
console.log(typeof this);
}
var spieler1 = {};
spieler1.sagMirWasDuBist();
var funktion = function() {};
funktion.sagMirWasDuBist();
Anmerkung: Diese Vorgehensweise wird aus den folgenden Gründen als Anti-Pattern eingestuft:
- Eingeführte Funktionen können in Konflikt kommen mit neu eingeführten ECMAScript-Funktionen
- In größeren Projekten können leicht Konflikte entstehen bei gleichen/doppelt belegten Funktionsnamen
- Es entsteht der Eindruck dass es sich um eine echte native JavaScript-Funktion handelt
Verwendung von Vererbung
Obwohl es in JavaScript die Möglichkeit gibt Vererbungshierarchien abzubilden wird dies oft in der Praxis nicht gemacht. Stattdessen folgen viele dem Paradigma Composition over Inheritance.
Kapselung
Kapselung ermöglicht den Zugriff auf Daten und Informationen zu steuern und auch zu verhindern.
Closures
In JavaScript (bis ECMAScript 5) kann man nur mithilfe von Closures echte Kapselung erreichen. Eine Closure wird erzeugt, wenn eine innere Funktion aus ihrem Erstellungskontext nach außen gegeben wird. Dadurch wird der Kontext in dem die Funktion definiert wurde so lange erhalten wie die Funktion selbst besteht.
var erzeugeLogFunktion = function(prefix) {
var logge = function(message) {
console.log(prefix + message);
};
};
var log1 = erzeugeLogFunktion('Nachricht:');
log1('Hello World');
var log2 = erzeugeLogFunktion('Message:');
log2('Hello World');
Mithilfe von Closures kann der Zugriff von Variablen innerhalb von Konstruktoren auf die inneren Funktionen beschränkt werden.
var Spieler = function(name) {
var kuerzel = name.charAt(0);
this.hatKuerzel = function(zuPruefendesKuerzel) {
return kuerzel === zuPruefendesKuerzel;
};
}
var spieler1 = new Spieler('John');
console.log(spieler1.hatKuerzel('J'));
Pseudo-private Eigenschaften
Closures sind schwer zu verstehen sein und erfordern einen sicheren Umgang mit JavaScript. Eine Pseudokapselung kann erreicht werden, indem vor Namen von privaten Eigenschaften ein Unterstrich vorangestellt wird.
var Spieler = function(name) {
this._name = name;
this._punktzahl = 0;
this.hatNamen = function(name) {
return this._name === name;
};
}
Anmerkung: Die Syntax hat keine Auswirkung auf den Zugriff, wird jedoch oft verwendet in der JavaScript-Commmunity.