In diesem Artikel geht es um JavaScript als Programmiersprache. Es werden Dinge wie Variablen, Funktionen, Objekte besprochen und wie man Verzweigungen, Schleifen oder Ähnliches baut.
Vorweg
Teilnehmer
Was Teilnehmer mitbringen müssen
Teilnehmer an diesem Kurs sollten grundsätzliche Erfahrungen in einer Programmiersprache mitbringen.
Wir besprechen Besonderheiten von JavaScript. Wir besprechen nicht, wie man programmiert. Wir besprechen, wie in JavaScript eine Variable angelegt wird, aber nicht, was eine Variable ist. Das Gleiche gilt für andere elementare Bausteine, wie Funktionen oder Kontrollstrukturen.
Der Kurs
Was man vorweg über den Kurs und die Doku wissen sollte
Der Kurs stellt in diesem ersten Teil JavaScript als Programmiersprache vor. In einem zweiten und dritten Teil wird die Einbindung von JavaScript in HTML und die Ereignisbehandlung beschrieben.
JavaScript als Programmiersprache hat einigen Stolperstellen und diversen Spezialitäten, über die man endlos schreiben und auch diskutieren kann.
Allerdings reicht schon ein kleiner Teil des Funktionsumfanges von JavaScript aus, um die Sprache im Zusammenhang mit HTML sinnvoll einzusetzen.
Im gesamten Kurs geht es um diese zentralen Sprachkonstrukte und darum, die Stolperstellen zu vermeiden. Die Spezialitäten sollen möglichst außen vor bleiben.
Handwerkzeug
Handwerkzeug, das man griffbereit haben sollte.
Das erforderliche Handwerkzeug für diesen Kurs ist überschaubar:
- Ein Texteditor
- Ein Browser
Texteditor
Der Texteditor muss kein wunderbares Ding sein.
Unter Windows empfiehlt sich notepad++ (der mitgelieferte notepad ist nicht geeignet).
Unter Apple empfiehlt sich CotEdit (der mitgelieferte TextEdit ist nicht geeignet).
Unter Unix tun es alle Standardeditoren. Von denen ist kate der komfortabelste, xed oder gedit sind aber auch geeignet.
Wer einen bestimmten Editor benutzen möchte: Bitte sehr.
Browser
Jeder moderne Browser ist in Ordnung.
Getestet wurden die Beispiele mit Chromium, Firefox und Web (teilweise auch mit Safari). Nicht getestet wurden die Browser auf Smartphones oder Tablets. Aber auch dort sollte alles funktionieren.
Die JavaScript Konsole
Für Ausgaben von Ergebnissen wird die JavaScript Konsole verwendet. Die Konsole ist Bestandteil der in Browsern enthaltenen "Entwicklerwerkzeuge" ("Diagnosefenster", "Entwicklerinformationen"). Sie dient dazu, Texte (Meldungen, Warnungen, etc.) anzuzeigen, die nicht in der eigentlichen Webseite auftauchen sollen.
Bei allen mir bekannten Browsern kann man die Entwicklerwerkzeuge über die Taste F12 (nicht Safari) öffnen. Dort gibt es einen Reiter oder Unterpunkt Konsole oder Console, in dem alle im JavaScript abgesetzten Meldungen erscheinen.
Das Gegenstück in JavaScript ist ein globales Objekt mit Namen "console", das nur dazu dient, etwas in das Diagnosefenster hinein zu schreiben.
Erzeugen einer Meldung für die Konsole
console.log("Irgendwas");JavaScript
Eigenschaften von JavaScript und was man von der Programmiersprache erwarten kann.
JavaScript ist eine Programmiersprache, die dazu erfunden wurde, HTML Seiten interaktiv zu gestalten.
- JavaScript ist eine interpretierte, nicht typisierte Programmiersprache.
- JavaScript Programme laufen im Browser.
- Die Laufzeitumgebung eines JavaScript Programms ist das HTML-Dokument, in dem das Programm eingebettet ist.
- Die ganz große Stärke von JavaScript ist die enge Einbindung in HTML und CSS.
- Ein laufendes JavaScript Programm hat unbeschränkten Zugriff auf das aktuelle HTML Dokument.
- Ein laufendes JavaScript Programm hat keinen Zugriff auf den Rechner.
Der letzte Punkt bedeutet, dass JavaScript nicht auf Dateien, Geräte oder andere Programme auf dem Rechner zuzugreifen kann. Den Rechner gibt es aus Sicht von JavaScript gar nicht.
JavaScript ausführen
Was man bei der Ausführung im Hinterkopf haben sollte.
JavaScript wird im Browser ausgeführt und benötigt eine HTML Seite, um zu laufen.
Für diesen Kurs werden die Programme bzw. Programmfragmente direkt in das HTML eingebunden. Der Programmcode steht zwischen den Marken <script>
und </script>
Diese Marken müssen sich entweder im head
oder body
befinden, jedoch weder davor, noch dazwischen, noch danach.
Skript in einer HTML Datei.
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JavaScript Test</title>
</head>
<body>
<h1>JavaScript Test</h1>
<p>Bitte Ausgabe in der Konsole ansehen (F12)
<script>
"use strict";
console.log("Test START");
</script>
</body>
</html>
Dieses Mini-HTML kann man kopieren und als Vorlage verwenden.
Wichtig, für diesen Kurs - und das wirkliche Leben - ist das merkwürdige Kommando am Kopf des Skriptes:
"use strict";
Es ist eine Anweisung an den Interpreter, das JavaScript im "strikten" Modus auszuführen. Es gibt auch den "normalen" (oder lockeren) Modus, der allen möglichen Programmierfehlern Tür und Tor öffnet. Den benutzen wir nicht, niemand tut das heute noch.
Bausteine
Bausteine der Programmiersprache
Datentypen
Datentypen in JavaScript - soweit es sie gibt.
JavaScript ist eine "untypisierte" Programmiersprache, d.h. man kann nicht formal bestimmen, welche Art von Daten in eine bestimmte Variable gehören und welche nicht. Das ändert nichts daran, dass es Datentypen gibt. Sie unterschieden sich dadurch, welche Werte zugelassen sind und was man damit machen kann.
In JavaScript gibt es sog. "primitive" Datentypen, die aus nur einem Wert bestehen, und zusammengesetzte Datentypen, die mehrere Wert enthalten können.
Primitive Datentypen
Die klassischen primitiven Datentypen.
- String, Text
- Besteht aus einem oder mehreren Buchstaben. Kann Sonderzeichen enthalten. Kann leer sein.
- Number, Zahl
- Ist eine Fließkommazahl. Ganze Zahlen sind ein Spezialfall ohne Nachkommastelle.
- Boolean
- Kann die Werte
true
undfalse
enthalten.
Spezialtypen
Es gibt zwei spezielle und recht merkwürdige Datentypen, die nur in JavaScript existieren.
- undefined
-
undefined
ist der Wert eines Ausdrucks, wenn er nicht definiert ist.undefined
ist ein Datentyp, der nur die Wertmengeundefined
kennt. - null
-
null
ist ein Wert der einem Ausdruck zugewiesen wird, wenn kein passender Wert ermittelt werden kann.null
ein Datentyp, der nur die Wertmengenull
kennt.
Zusammengesetzte Datentypen
Datentypen, die aus mehreren anderen Datentypen bestehen.
- Objekte
-
Objekte bestehen aus ein oder mehreren Attributen und können Funktionen enthalten. Objekte "klammern" mehrere Variablen und Funktionen, die in einem gemeinsamen Kontext Sinn machen. Es gibt zwei verschiedene Sorten Objekte:
- JavaScript Objekte werden "on the fly" angelegt und instanziiert.
- "Richtige" OO Objekte werden in einer Klassendefinition oder Konstruktorfunktion deklariert und mit dem Operator
new
instanziiert.
- Kollektionen, Arrays
-
Kollektionen sind eine Sammlung von vielen Werten unter einem Dach. JavaScript kennt
Map
undSet
als echte Kollektionen, basierend auf einer Klassenspezifikation undArray
als eine Art Mittelding zwischen Objekt und eingebautem Datentyp.
Bestimmung von Datentypen
Wie finde ich den Datentyp eines Ausdrucks.
Es gibt einen Operator mit Namen typeof
. Damit kann man den Typ einer Variablen abfragen.
Benutzung von typeof
console.log ("Hallo", typeof ("Hallo"));
Er dient dazu, herauszufinden, zu welchem Datentyp ein Ausdruck (eine Variable) gehört.
Zum Nachsehen und Ausprobieren bitte diesen Link typeof
benutzen öffnen und F12 drücken.
Der Operator typeof
ist aber nicht wirklich informativ. Wer von Java oder C++ kommt, wird Erwartungen zurückschrauben müssen, mächtig. Der Operator erkennt die primitiven Datentypen und er erkennt Funktionen. Aber ansonsten ist alles "object".
Variablen
Variablen.
Variablen sind benannte Platzhalter für Werte. Sie haben immer genau einen Wert, der gesetzt und abgefragt werden kann.
Deklaration
let
Variablen werden angelegt mit dem Schlüsselwort let
.
Anlegen von Variablen
let myNumber = 42;
let myNumber2 = 47.11;
let myNumber3 = 08 / 15;
let myVar;
Nach der letzten Anweisung ist myVar
im Zustand "undefined" und wartet auf eine Zuweisung.
myNumber3
ist angelegt und mit einer Rechenoperation initialisiert.
const
So komisch das klingt: Variablen können auch konstant sein.
Eine Konstante ist eine Variable, der direkt beim Anlegen ein einziges Mal ein Wert zugewiesen wird. Danach kann ihr kein anderer Wert zugewiesen werden.
Eine Variable wird zu einer Konstanten, indem sie mit dem Schlüsselwort const
angelegt wird.
Deklaration und Definition einer Konstante
Variablen und Datentypen
Der Zusammenhang in JavaScript.
Variablentypen gibt es in JavaScript nicht; es ist eine "untypisierte" Sprache.
Eine Variable ist immer von dem Typ des ihr gerade zugewiesenen Wertes.
In JavaScript können ein und derselben Variable zu unterschiedlichen Zeiten Werte zugeordnet werden, die von unterschiedlichem Typ sind.
Variablen mit Werten von verschiedenen Typen können in Ausdrücken auch munter miteinander vermischt werden. Man muss dann allerdings ziemlich aufpassen, dass das Ergebnis auch noch plausibel ist bzw. dem entspricht, was man erwartet.
Zuweisung von Variablen mit skalaren Werten Mixtur
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JavaScript: Zuweisungen</title>
</head>
<body>
<h1>JavaScript: Zuweisungen</h1>
<p>Ausgabe in der Console beachten</p>
<script>
let x;
console.log("x", x);
// undefined
x = null;
console.log("x", x);
// null
x = "Hallo X";
console.log("x", x);
// Hallo x
let y = 1;
let z = x + y;
console.log("z", z);
// Hallo X1
x = 2;
z = x + y;
console.log("z", z);
// 3
x = "2";
z = x + y;
console.log("z", z);
// 21
</script>
</body>
</html>
Die gezeigte Sequenz von Anweisungen ist in einem HTML Dokument im <body>
mit <script> ... </script>
eingebaut. Diese Konstruktion sollte man in der Praxis nicht benutzen.
Die Variable x durchläuft verschiedene Zustände. Nach der nackten Deklaration mit let x;
ist sie zwar vorhanden, aber "undefiniert".
Die nächste Anweisung, x = null;
hilft ein kleines Stück weiter. Jetzt ist sie definiert, hat aber einen Wert, der besagt, dass sie keinen Wert hat. null
bedeutet: kein Wert.
Erst im dritten Schritt bekommt sie einen Wert, mit dem man etwas anfangen kann.
Die anschließende Operation mit y zeigt, dass man problemlos einer Variablen, die vorher Text enthalten hat, später eine Zahl zuweisen kann.
Die letzten beiden Anweisungen mit der Variablen z spielen mit den Wertetypen Text und Zahl.
"Addiert" man zu einer Variablen, die Text enthält, eine Zahl, so wird die Addition als Zusammenfügen von zwei Texten interpretiert. In dem konkreten Beispiel wird aus x + 1
die Textverbindung "Hallo X" + "1".
Führt man die Addition dagegen mit zwei numerischen Werten aus, y + 1
, so wird daraus 1 + 1, ein mathematischer Ausdruck, mit dem tatsächlich gerechnet werden kann. Das Ergebnis, 2
, wird der Variablen z zugeordnet. Die hat zwar vorher Text enthalten hat, aber durch diese Zuweisung wird sie zu einer numerischen Variablen, mit der bei Bedarf weitere mathematische Operationen durchgeführt werden können.
Das auf den ersten Blick sehr entgegenkommende Verhalten von JavaScript birgt durchaus seine Gefahren und führt nicht selten zu unerwarteten Ergebnissen.
Versuchen Sie, das Mixen von Typen zu vermeiden. Wenn Sie doch mixen, verlassen Sie sich nicht darauf, dass das Ergebnis auch dem entspricht, das Sie erwartet haben.
Die Werte, die man einer Variablen zuweisen kann, sind buchstäblich beliebig. Es können Arrays sein, Knoten des DOM, Objekte oder Inhalte eines HTML Elementes. Es ist möglich, das ganze HTML Dokument einer Variablen zuzuweisen und danach irgendwelche Operationen damit anzustellen.
Aufgaben:
- Versuchen Sie eine Konstante ohne initialen Wert anzulegen.
- Probieren Sie die Addition von Text und Zahlen in verschiedenen Kombinationen aus.
- Probieren Sie Grenzfälle, wie ("10" + "1"), (10 + 1), ("10" + 1), (10 + "1") und beobachten Sie die Ergebnisse.
- Was passiert bei Subtraktion (10 - 1) und was bei ("10" - "1")? Und was bei ("Hallo" - "o")?
Funktionen
Funktionen, Prozeduren, Unterprogramme
Funktionen sind eine Zusammenfassung von Anweisungen unter einem Namen.
Funktionen können - müssen aber nicht - beim Aufruf Werte übergeben bekommen. Sie können - müssen aber nicht - einen Wert beim Beenden zurück geben.
In JavaScript können Funktionen auf zwei Arten angelegt werden: Per Deklaration und als Ausdruck.
Funktionsdeklaration
Anlegen einer Funktion per Deklaration
Im Normalfall werden Funktionen angelegt, indem der Name und die Parameter deklariert und gleichzeitig ihr Körper - das, was die Funktion macht - definiert wird.
In anderen Programmiersprachen haben die Begriffe "Deklaration" und "Definition" eine ganz klare Bedeutung. In JavaScript sind sie etwas unscharf, können aber manchmal trotzdem hilfreich sein.
Anlegen und Aufrufen einer Funktion
{
console.log("myFunc:", a, b);
return "fertig";
}
let x = myFunc("hallo", "tach");
console.log("result:" + x);
Die Funktionsdeklaration beginnt mit dem Schlüsselwort function
gefolgt von einem frei wählbaren Namen und einem Klammerpaar. In den Klammern können optional Parameter definiert werden. Danach kommt zwischen den geschweiften Klammern ein Block von Anweisungen, also das, was die Funktion macht.
In dem Beispiel gibt es die Funktion myFunc(...)
, die zwei Parameter hat. Sie enthält eine Anweisung zur Ausgabe der übergebenen Werte und danach eine Anweisung mit dem Schlüsselwort return
, die dem Aufrufer "fertig" mitteilt.
Der Aufruf der Funktion erfolgt mit der Anweisung x = myFunc(...)
. In den Klammern werden die aktuellen Werte übergeben. Der Rückgabewert der Funktion wird in der Variablen x aufgefangen, mit der danach weiter operiert werden kann.
Mit diesem kurzen Szenario sind schon die meisten Anwendungsfälle erschlagen. Zwar noch nicht die angestrebten 80%, aber bestimmt über 50%.
Die Funktion myFunc(...)
erwartet beim Aufruf zwei Werte, und liefert einen Wert zurück.
Man kann die Funktion aufrufen, ohne das Ergebnis abzufragen. Dann steht der Rückgabewert eben einfach nicht zur Verfügung.
Man kann sie auch mit weniger oder mehr Werten aufrufen. Im ersten Fall - zu wenige Werte - werden die fehlenden Werte mit "undefined" belegt und sind somit innerhalb der Funktion nutzlos. Im zweiten Fall - zu viele Werte - werden die überzähligen Werte einfach ignoriert.
Das Ignorieren des Rückgabewertes macht in manchen Fällen Sinn, die Übergabe einer falschen Anzahl von Werten dagegen nur selten.
Es gibt Möglichkeiten, auf überzählige Parameter zuzugreifen und es gibt Funktionen, die eine beliebige Anzahl von Parametern erwarten und bearbeiten können. Das sind Spezialfälle, die hier nicht behandelt werden.
Fehlwerte
Weniger speziell ist es, wenn man beim Schreiben einer Funktion schon erwartet, dass der Aufrufer nicht alle Parameter sinnvoll füllen kann. Das lässt sich bei der Deklaration durch sogenannte "Fehlwerte" (engl. default values) abfangen. Der "Fehlwert" ist wörtlich der Wert, der genommen wird, wenn ein Wert "fehlt".
Ein etwas unsinniger Anwendungsfall könnte eine Begrüßungsfunktion sein.
Begrüßung mit Standardwerten
{
console.log("halloTach.", text, an);
}
halloTach();
halloTach("Hallo");
halloTach("Gute Nacht", "mein Mäuschen");
halloTach(undefined, "Herr Meier");
Funktionen und Variablen
Es wurde schon gesagt, dass man einer Variablen so gut wie alles zuweisen kann. Auch eine Funktion.
Zuweisung einer Funktion an eine Variable
console.log("myFunc");
return "fertig";
}
let x = myFunc();
let f = myFunc;
x = f();
Im oberen Bereich wird mal wieder die Funktion "myFunc" angelegt, die nicht viel tut, aber "fertig" meldet, wenn sie fertig ist.
Die nächsten beiden Anweisungen legen Variablen fest, die beide irgendwie auf "myFunc" verweisen. Der optisch kaum sichtbare, inhaltlich aber riesige, Unterschied besteht in den Klammern am Ende, einmal mit, einmal ohne.
Der Ausdruck let x = myFunc()
führt die Funktion aus und liefert das Ergebnis an x zurück.
Der Ausdruck let f = myFunc
(ohne Klammern) legt die Variable f an und weist ihr den Funktionskörper von myFunc zu. Ein kleiner Unterschied mit großen Folgen.
Die Variable f kann jetzt benutzt werden, um die ursprünglich in myFunc gespeicherte Funktion auszuführen. Das passiert in der letzten Anweisung x = f()
. Das Ergebnis ist identisch mit dem eines direkten Aufrufs x = myFunc()
.
Laut offizieller Dokumentation wird bei der Deklaration einer Funktion - versteckt und automatisch - eine Variable mit demselben Namen angelegt.
Funktionen als Argumente für Funktionen
Wenn man eine Funktion in einer Variablen speichern kann, kann man sie auch als Argument an eine andere Funktion übergeben.
Übergabe einer Funktion als Parameter
console.log(p1, p2);
return "fertig";
}
function print(pf, a, b)
{
let x = pf(a, b);
return x;
};
let x = print(myFunc, 10, 11);
Unsere "myFunc" gibt es hier in einer Version mit Parametern. Sie macht nichts weiter, als die Inhalte ihrer Parameter in der Console auszugeben und "Fertig" zurück zu liefern.
In dem Beispiel gibt es zusätzlich eine Funktion print. Sie soll etwas drucken, das sagt der Name. Sie soll den Inhalt von a und b drucken. Der Funktion print wird aber nicht nur der Inhalt von außen vorgegeben, sondern auch wie der Druckvorgang ablaufen soll (Papier, Bildschirm oder Papierkorb). Das wird in myFunc erledigt und print bekommt myFunc im Parameter pf als Argument übergeben.
Funktionsausdruck
Anlegen einer Funktion als Zuweisung zu einer Variablen
Bei einem Funktionsausdruck (function expression) dreht es sich darum, eine Funktion gar nicht erst auf herkömmliche Art zu deklarieren, sondern die Deklaration gleich und explizit einer Variablen zuzuweisen.
Anlegen einer Funktion per Zuweisung an eine Variable
console.log(p1, p2);
return "fertig";
};
let x = myFunc(1, 2);
Vergleicht man diese Syntax mit der, die wir schon kennen, ist kaum ein Unterschied festzustellen.
Beim normalen Umgang mit Funktionen ist auch nicht viel Sinn dahinter. Diese Syntax wird hier vorgestellt, weil sie
- für Funktionen innerhalb von Objekten (Methoden) gebraucht wird,
- syntaktisch den Umgang mit anonymen Funktion vorbereitet.
Anonyme Funktionen
"Anonyme Funktionen" sind Funktionen, die keine Namen haben und keiner Variablen zugewiesen sind. Man kann sie nicht ansprechen und damit auch nicht aufrufen.
Sinnlos? Normalerweise ja.
Es sei denn, man braucht eine Funktion nur, um sie an eine andere Funktion als Argument zu übergeben. In diesem Fall kann man den Rumpf der zu übergebenden Funktion direkt in den Funktionsaufruf schreiben.
Auch wenn übergebene Funktion beim Aufruf keinen Namen erhält, ist sie in der aufgerufenen Funktion über den dort definierten Parameternamen ansprechbar.
Diese Art von Funktionsdefinition "on the fly" ist geeignet, wenn woanders eine Funktion als Argument erwartet wird, und diese Funktion sonst nirgendwo gebraucht wird.
Die Alternative wäre, im aufrufenden Kontext extra eine Funktion anzulegen. Dort wird sie aber gar nicht gebraucht. Die liegt dann da herum, tut nichts, nützt nichts. Müll. Anonyme Funktionen sind ein gutes Beispiel für "Code-Hygiene".
Es macht Sinn, anonyme Funktionen einzusetzen, wenn:
- Es syntaktisch notwendig ist, an einer bestimmten Stelle eine Funktion zu verwenden.
- Die Funktion einen überschaubaren Körper hat.
- Die Funktion nur an dieser einen Stelle verwendet wird.
Das Ganze macht keinen Sinn, wenn:
- Der Code in der übergebenen Funktion sehr kompliziert oder einfach lang ist.
- Die übergebene Funktion mehrfach verwendet wird.
Syntax
Die Syntax bei der Verwendung von anonymen Funktionen ist etwas sperrig und man muss Klammern zählen.
Verwendung einer deklarierten und einer anonymen Funktion
function someFuncExec (f)
{
f();
}
// Erstellen einer Funktion, die etwas in der Console ausgibt
function someFunc()
{
console.log("Ich bin someFunc");
}
// Aufruf mit Übergabe einer Funktion als Variable
someFuncExec(someFunc);
// Aufruf mit Übergabe einer anonymen Funktion
someFuncExec(function () {
console.log("Hallo Tach! Ich bin eine anonyme Funktion");
});
Hier wird erstmal eine Funktion someFuncExec
angelegt, die eine Funktion als Argument erwartet und sie dann ausführt.
Danach wir die Funktion someFunc
deklariert, die etwas in der Konsole ausgibt.
So weit, so gut.
Mit someFuncExec(someFunc)
wird die erste Funktion mit dem Namen der zweiten Funktion aufgerufen. Wichtig ist es, zu sehen, das der Funktionsname someFunc ohne Klammern übergeben wird. Damit ist der Name ein Verweis auf den Funktionskörper.
Im letzten Schritt erst kommt eine anonyme Funktion ins Spiel. Das Statement someFuncExec(function ...)
ruft someFuncExec auf und übergibt einen Funktionsrumpf - und zwar nicht als Variable, sondern als direkte Deklaration. Die Deklaration ist einmalige und anonym.
Anonyme Funktionen können auch Parameter haben.
Anonyme Funktion mit Parametern
// eine Funktion als Parameter erwartet,
// die selber Parameter erwartet
function someFuncExec2 (f)
{
let x = 2 * 2; // Komplizierte Berechnung
let y = 17 + 4; // Komplizierte Berechnung
f(x, y);
}
// Aufruf mit Übergabe einer anonymen Funktion, die eigene Parameter hat
someFuncExec2(function (p1, p2) {
console.log("anonyme Funktion mit zwei Parametern", p1, p2);
});
Die Parameter müssen bei der Deklaration festgelegt werden.
Anonyme Funktionen können nicht nur Parameter haben, sie können auch Werte zurück liefern.
Anonyme Funktion mit Parametern und Rückgabewert
// eine Funktion als Parameter erwartet und
// einen Rückgabewert liefert
function someFuncExec3 (f)
{
let x = 10; // Komplizierte Berechnung
let y = 20; // Komplizierte Berechnung
return f(x, y);
}
// Aufruf mit Übergabe einer anonymen Funktion, die
// eigene Parameter hat und
// einen Wert zurück liefert
let x = someFuncExec3(function (p1, p2) {
let ret = p1 + p2;
console.log("anonyme Funktion mit zwei Parametern und Rückgabewert", p1, p2, ret);
return ret;
});
console.log("Ergebnis dumFuncExec3", x);
Diese Verwendung der anonymen Funktion kommt sehr häufig vor.
Pfeilnotation
Neben der beschriebenen Syntax, bei der die Deklaration einer anonymen Funktion mit dem Schlüsselwort function
in der Klammer stattfindet, gibt es eine verkürzte Schreibweise, die sich "Pfeilnotation" nennt.
Die Verwendung der Pfeilnotion ist generell ungewohnt und etwas trickreich, wenn es um Grenzbereiche geht. Wer in diese Grenzbereiche hinein will, sollte auf jeden Fall den Artikel Arrow function expressions bei MDM lesen.
Hier wird die Standardvariante der Notation vorgestellt, die für 99,9% aller Fälle die richtige ist.
Das letzte Beispiel mit dem Aufruf der Funktion someFuncExec3
mit Parametern und Rückgabewert sieht mit Pfeilnotation folgendermaßen aus:
Anonyme Funktion mit Parametern und Rückgabewert in Pfeilnotation
// eigene Parameter hat und
// einen Wert zurück liefert
// in Pfeilnotation
x = someFuncExec3((p1, p2) => {
let ret = p1 + p2;
console.log("Pfeilnotation", p1, p2, ret);
return ret;
});
console.log("Ergebnis dumFuncExec3 Pfeilnotation", x);
Statt mit dem Schlüsselwort function
beginnt die Deklaration der anonymen Funktion in Pfeilnotation gleich mit den Klammern und den Parametern (p1, p2)
. Danach gibt es den Pfeil =>
, der auf den Funktionsrumpf zeigt, der wie gehabt in den geschweiften Klammern { ... }
steht. Dieser Pfeil ist verantwortlich für die Namensgebung.
Einsatz
Anonyme Funktionen werden sehr viel im Bereich Ereignisbehandlung verwendet. Das ist keine JavaScript spezifische Angelegenheit, sondern hängt damit zusammen, dass in graphischen Benutzeroberflächen dauernd auf Tonnen von Ereignissen reagiert werden muss. Die Reaktion besteht häufig nur aus sehr wenigen Zeilen Programmcode, der aber in Form einer Funktion an sogenannte "Event Handler" übergeben werden muss.
Objekte
In JavaScript ist von Hause aus keine OO (Object Oriented) Sprache, trotzdem begegnet man "Objekten" auf Schritt und Tritt.
Es gibt seit einiger Zeit in JavaScript auch Klassen, die über das Schlüsselwort class
angelegt werden. Arbeitet man mit Objekten, die aus einer class erzeugt wurden, kommt man der klassischen OO Welt schon sehr nahe.
Hier beschäftigen wir uns mit den eher leichtgewichtigen Objekten, die JavaScript von Anfang an kennt und die schlicht "object" genannt werden.
Objekte kapseln Daten, die logisch zusammen gehören aber auf mehrere Variablen verteilt sind. Immer dann, wenn etwas verbal mit "XYZ besteht aus a, b und c" oder einfach "XYZ hat a, b und c" beschrieben werden kann, ist "XYZ" ein ein Kandidaten für ein Objekt und "a", "b" und "c" sind seine Attribute.
Zusätzlich ist es auch möglich, in den Objekten Funktionen zu definieren, die nur im Zusammenhang mit genau diesem Objekt Sinn machen. Die in einem Objekt gekapselten Funktionen heißen "Methoden".
Objekte bestehen aus Daten und Funktionen, die logisch zusammenhängen und unter einem Namen zusammengefasst werden.
Gängige Beispiele für einen solchen Einsatz sind Personen oder Adressen.
"Eine Person hat einen Vornamen, einen Nachnamen, ein Geburtsdatum und eine Adresse."
"Eine Adresse besteht aus Straße, PLZ und Ort"
Wenn in einem Programm mehrere Personen abgebildet werden müssen (Verkäufer, Käufer, Vater, Tochter, ...) und wenn man die dazu gehörenden Daten nicht in Objekten kapselt, kommt man in derbe Schwierigkeiten. Die Anzahl an Variablen, die man dafür braucht, explodiert ziemlich schnell. (vaterName, vaterVorName, tochterName, tochterVorname ... plus die anderen Attribute. Und das für jede Person, mit der man es zu tun bekommt.)
Objekte - auch in der originalen JavaScript Version - schaffen Abhilfe, indem man für jede Person ein Objekt anlegt, das die Attribute Name, Vorname, Geburtsdatum und Adresse kapselt, die über den Namen des Objektes ansprechbar sind.
Literale Objekt
Anlegen eines leeren Objektes
//Überprüfen
console.log("myObj:", myObj, "Typ: ", typeof(myObj));
Der Ausdruck legt ein leeres Objekt mit dem Namen myObj an, mit dem man rein gar nichts machen kann, außer ihm Attribute zuzuweisen.
Syntaktisch bedeutsam sind die geschweiften Klammern. Sie teilen JavaScript mit, dass es sich bei der leeren Variable um ein Objekt handelt. Damit ist es möglich, mit myObj Operationen durchzuführen, die nur mit Objekten gehen.
Anlegen von Attributen und Funktionen in einem Objekt
myObj.attrib2 = "Tach";
myObj.func1 = function() { console.log(this.attrib1, this.attrib2) };
//Überprüfen
console.log("myObj:", myObj, "Typ: ", typeof(myObj));
Hier werden in dem leeren Objekt zwei Attribute und eine Funktion angelegt und ihnen ein Wert zugewiesen.
Für diese Art der Zuweisung ist syntaktisch keines der Schlüsselworte const
, let
oder var
zulässig.
Man beachte, dass das funktioniert, obwohl das Objekt als const
deklariert wurde. const macht die Variable konstant, nicht ihren Wert.
Die Implementierung der Funktion folgt dem Muster, das schon bei der Schaffung einer Funktion durch Zuweisung benutzt wurde.
Zugriff auf Attribute und Funktionen
let a2 = myObj.attrib2;
myObj.attrib1 = "Guten";
myObj.attrib2 = "Morgen";
// Funktion aufrufen
myObj.func1();
Für den Zugriff auf Elemente eines Objektes dient der Operator Punkt (".") : Objektname.Elementname
. Die Form gilt sowohl für den Zugriff auf Attribute, als auch für Funktionsaufrufe.
Sinnvoller, als das Anlegen eines leeren Objektes mit anschließender "Auffüllung", ist es, das Objekt gleich komplett zu konstruieren. Zwar ist das immer noch nicht das Ende der Fahnenstange, aber ein Schritt in die richtige Richtung.
Anlegen eines Objektes mit Attributen und Funktionen
attrib1: "Hallo",
attrib2: "Tach",
func1: function() { console.log(this.attrib1, this.attrib2) },
}
// Überprüfen
console.log("myObj2:", myObj2, "Typ: ", typeof(myObj2));
Hier sind folgende Dinge beachtenswert:
- Attribute müssen bei der Deklaration initialisiert werden.
- Die Initialisierung erfolgt durch den Doppelpunkt hinter den Attributen gefolgt von einem Wert.
- Der Zugriff auf Attribute înnerhalb der Objektdeklaration erfolgt über das Schlüsselwort
this
gefolgt von einem Punkt gefolgt vom Namen des Attributes.
Attribute in einem Objekt müssen immer initialisiert werden. Man kann sie mit null
oder undefined
initialisieren, aber gemacht werden muss es, sonst gibt es einen Syntaxfehler.
Die Initialisierung wird durch ":" eingeleitet, nicht durch "=". Es wird wahrscheinlich geheime Gründe dafür geben, warum das so ist, auf jeden Fall ist es syntaktisch notwendig.
Diese Art von Initialisierung spielt nur bei der literalen Anlage von Objekten eine Rolle. Das kommt zwar nicht so häufig vor, aber man muss es wissen.
Was aber im Zusammenhang mit Objekten immer gebraucht wird, ist die Dereferenzierung über das Schlüsselwort "this". Mit dem Ausdruck this.xx
greift man von innen auf das Attribut xx zu, so wie man mit objName.xx
von außen darauf zugreift.
Lässt man das this weg, sucht JavaScript nach einer Variable, die sich außerhalb des Objekts befindet, benutzt sie, wenn sie da ist und meckert, wenn nicht.
Das gilt für alle Objekte, egal wie sie konstruiert wurden.
Objektfabriken
Der entscheidende Haken an der bisher vorgestellten Konstruktion von Objekten ist, dass man alles wiederholen muss, wenn man ein neues Objekt anlegen will.
Der nächste Schritt, den Umgang mit Objekten zu rationalisieren, ist, die Konstruktion in eine Funktion zu packen und sich ein fertiges Objekt zurück liefern zu lassen
Funktion zur Konstruktion eines Objektes
{
const obj = {
attrib1: null,
attrib2: null,
func1: function() {
console.log(this.attrib1, this.attrib2)
},
};
return obj;
}
const o = createObj();
const o1 = createObj();
Kein Mensch macht das so. Aber es ist in dieser Form gut ersichtlich, was innen drin passiert.
Innerhalb der Funktion, wird - ganz wie gehabt ein Objekt - definiert und dabei einer Variablen zugewiesen. Anschließend wird das Objekt in der Variablen zurück gegeben.
Außerhalb kann die Funktion jetzt zig mal aufgerufen werden. Sie baut jedes mal ein neues Objekt zusammen und liefert es zurück.
Wenn man schon den Weg geht, die Konstruktion von Objekten in Funktionen zu packen, sollte man es auch richtig machen und die Funktion entsprechend ausstatten.
Funktion zur Konstruktion eines Objektes (richtig)
{
return {
teil1: p1,
teil2: p2,
sprich: function() {
console.log(this.teil1, this.teil2);
},
};
}
Die Fabrikfunktion hat Parameter bekommen. Sie entsprechen den Attributen des Objektes. Die Parameter haben Fehlwerte, die das Objekt auch dann sinnvoll füllen, wenn keine Argumente übergeben werden.
Die Definition des Objektes wurde in das return
Statement verlagert, um Zeilen zu sparen
Mit der Funktion lassen sich beliebig viele Gruß-Objekte anlegen und die Art des Grußes kann dabei bestimmt werden.
Verwendung
const gAbends = createGruss("Guten", "Abend");
const gNacht = createGruss("Gute", "Nacht");
const gWeissNicht = createGruss();
const gLeer = createGruss(null, null);
gMorgens.sprich();
gWeissNicht.sprich();
gLeer.sprich();
Die Objektfabrik lässt sich jetzt dazu verwenden, jede Menge Grußbotschaften zu generieren. Die Objektfunktion sprich()
schreibt sie in die Konsole.
Bei dem "gWeissNicht" Gruß wird auf die Fehlwerte zurück gegriffen, weilkeine Argumente übergeben wurden.
Die Botschaft mit dem Namen "gLeer" ist dagegen tatsächlich leer.
Da die Parameter mit Fehlwerten belegt sind, muss man, wenn man ein leeres Objekt haben will, explizit null
übergeben
Arrays
Arrays sind zur Massendatenverarbeitung gedacht. Sie speichern eine - nahezu - beliebige Menge Daten in einer geordneten Reihe.
Arrays sind die flexibelsten und am häufigsten gebrauchten Container. Bei Unsicherheit: Array. Funktioniert (fast) immer.
Arrays werden meistens als reines Datenfeld mit wahlweisem Zugriff benutzt. Sie bieten allerdings auch Funktionen, mit denen man einen Stack oder eine Queue darstellen kann.
Array als "Datentyp"
Ein Array ist in JavaScript, wie in anderen Programmiersprachen auch, ein Datenfeld von hintereinander stehenden konkreten Werten.
Im einfachen Fall ist ein Array eindimensional. Da Arrays geschachtelt werden können, lassen sich damit aber auch Matrizen oder Kuben bauen.
In einem Array sind die darin enthaltenen Elemente geordnet: Jedes steht an einer bestimmten Position und kann über die Positionsnummer, den Index, adressiert werden.
Der Index bezieht sich auf ein "Fach" im Array, nicht auf die Ordnungsnummer, die ein bestimmtes Element bekommt, wenn man die Elemente nacheinander durchgeht.
Nur wenn ein Array komplett gefüllt ist, sind Position und Ordnungsnummer identisch.
Es hilft vielleicht, sich ein Array als CD Regal vorzustellen.
Das Regal fängt links an einer Wand an und zieht sich nach rechts bis zu einem bestimmten Punkt.
Das Regal hat Fächer, in denen die CD's stehen.
Das erste Fach, ganz links, hat die Nummer 0, das letzte Fach, ganz rechts, hat die Nummer length - 1.
Die CD's werden über die Fächer adressiert. Nicht alle Fächer müssen CD's enthalten.
Wenn das Regal voll ist, kann nach rechts angebaut werden.
Array per Hand anlegen
Die markanten Zeichen im Umgang mit Arrays sind die eckigen Klammern "[" und "]". Sie werden nicht nur beim manuellen Anlegen eines Arrays benutzt, sonder auch für den gezielten Zugriff auf einzelne Elemente.
Gezielter Zugriff auf einzelne Elemente eines Array
myArray[3] = "vier";
Die Positionen, der Index, werden von 0 aufwärts gezählt. Diese Positionsnummern sind lückenlos, auch wenn das Array an manchen Stellen nicht enthält.
Da das erste Fach im Array an der Stelle 0 steht, liefert der Zugriff auf myArray[1]
die "zwei" und nicht "eins".
Wichtig: Der numerischen Index beginnt mit 0 und nicht mit 1. Das ist etwas, das man im Hinterkopf behalten muss.
Obwohl das Array nur in den drei Positionen 0 .. 2 definiert ist, führt die Zuweisung an myArray[3]
nicht zu einem Fehler. JavaScript legt in diesem Fall ein neues Element an der Stelle 3 an und weist ihr den Wert "vier" zu.
In einem JavaScript Array sind unterschiedliche Datentypen erlaubt, so dass auch folgendes geht:
Array mit verschiedenen Datentypen
Das Objekt "Array"
Arrays dienen der Speicherung von beliebig großen Datenmengen - begrenzt natürlich durch die Physik.
Allerdings sind Arrays auf denselben Fundamenten aufgebaut, wie Objekte und haben neben den eigentlichen Daten weitere Attribute und Methoden.
Ein immer wieder benutztes Attribut eines Arrays ist length
. In diesem Wert ist die aktuelle Länge des Arrays enthalten. Genauer: Die höchste besetzte Position + 1.
Benutzung von array.length
myArray.length = 3;
Bei der ersten Zuweisung wird die Länge abgefragt. Ok.
Bei der zweiten Anweisung wird die Länge gesetzt. Das bedarf einer Erklärung. Wenn die neue Länge größer ist, als die alte, sind die Positionen bis zur neuen Länge schlicht undefined
. Ist sie kürzer, als die alte, sind alle Einträge jenseits der neuen Länge verschwunden.
Das Attribut length
wird häufig in Schleifen benutzt, manchmal auch nur, um zu sehen, ob in dem Array überhaupt etwas drin ist.
Zwei nützliche Funktionen eines Array-Objektes sind array.push(Wert)
und x = array.pop()
.
Die Funktion push(Wert)
hängt den als Parameter übergebenden Wert an das Array an. Die Länge das Array wird dabei erweitert.
Die Funktion pop()
liefert den letzten Wert aus dem Array zurück und entfernt ihn dabei. Die Länge des Array wird um eins gekürzt.
Benutzung von length
, push()
und pop()
console.log("myArray: ", myArray, "Len: ", myArray.length);
myArray.push("gepushed");
console.log("myArray: ", myArray, "Len: ", myArray.length);
let x = myArray.pop();
console.log("x: ", x);
console.log("myArray: ", myArray, "Len: ", myArray.length);
Hier wird mit myArray.length = 10;
brutal die Länge des Array gesetzt. myArray hat jetzt 10 Fächer. Ansonsten wird die Länge zur Kontrolle als Ausgabe in der Konsole benutzt.
Das Statement myArray.push("gepushed");
hängt das schicke, neudeutsche Wort "gepushed" als elftes Element an der Stelle 10 hinten an das Array an. myArray hat jetzt die Länge 11.
Das Statement x = myArray.pop();
holt - "zieht" - genau dieses Element wieder aus dem Array heraus und lädt es in die Variable x. In x steht dann "gepushed" und myArray hat wieder die Länge 10.
Ein Array mit 10 leeren Werten hat die Länge 10 und der letzte Index ist 9.
Ein pop()
auf dieses Array würde den letzten, leeren, Wert liefern und ein Array mit 9 leeren Werten zurück lassen.
Gültigkeitsbereiche
Variablen (und Funktionen und Objekte und ...) haben einen gültigen Anwendungsbereich, manchmal auch "Gültigkeitsbereich" genannt. Der Fachbegriff auf Englisch heißt "scope". Außerhalb ihres Gültigkeitsbereiches sind Variablen nicht einfach ungültig: Sie sind schlicht und ergreifend gar nicht da. Sie sind nicht definiert, "undefined".
Global/Lokal
Globaler Scope und lokaler Scope.
Der wichtigste Scope ist der globale Scope. Das ist das Dokument, in dem das Script eingebunden ist.
Tut man nichts besonderes und legt in in einem Skript eine Variable an, so befindet sie sich im "globalen Scope". Deklariert man die Variable innerhalb eines Blocks (z.B. in einer Funktion), befindet sie sich in einem "lokalen Scope".
Globaler Scope und Block Scope
"use strict";
// Ĝlobaler Scope
let ups = 10;
console.log("ups global:", ups);
{ // Block Scope
console.log("ups im Block:", ups);
let ups2 = 20;
console.log("ups2 im Block:", ups2);
}
// Achtung: ups2 ist hier unbekannt
console.log("ups2 global:", typeof(ups2));
</script>
In diesem kurzen Skript wird die Variable ups im Globalen Scope angelegt und mit dem Wert 10 initialisiert. In diesem Fall ist das Script in einem HTML Dokument. Der JavaScript Code befindet sich innerhalb der Marken <script>
und </script>
. Die Varaible ups liegt direkt unterhalb der Marke <script>
und damit im globalen Scope.
Grundsätzlich gilt, dass eine Anweisung immer dann im globalen Scope ist, wenn links oder oberhalb der Anweisung keine öffnenden geschweifte Klammer, {
, existiert, die noch nicht geschlossen wurde.
Die geschweiften Klammern in der Mitte definieren einen "Block". Ein Block definiert einen eigenen Scope.
Dinge, die in diesem Block angelegt werden, sind außerhalb nicht sichtbar, nicht vorhanden, nicht definiert.
Deshalb ist die Variable ups2 zwar innerhalb des Blocks vorhanden und benutzbar, außerhalb jedoch nicht.
Andersherum ist die globale Variable ups innerhalb des Blockes durchaus sichtbar und kann auch verwendet werden.
Ein Block schafft also einen privaten Raum: raus gucken geht, rein gucken geht nicht. Das gilt nicht nur für den globalen Scope bezogen auf einen inneren Block, sonder gilt generell für alle Schachtelungen von Blöcken: In der Richtung nach Außen sind Dinge sichtbar und benutzbar, in der Gegenrichtung nicht.
Überdecken
Innerhalb eines Blockes ist es möglich eine Variable anzulegen, die denselben Namen hat, wie eine Variable, die außerhalb des Blockes definiert wurde. Das führt nicht zu einen Namenskonflikt, sondern dazu, dass die innere Variable die Äußere verdeckt.
Überdecken von Variablen
console.log("ups global:", ups);
{
let ups = 20;
console.log("ups im Block:", ups);
}
console.log("ups global:", ups);
Die Variable ups wird im globalen Scope angelegt und bekommt den Wert 10.
In dem durch die geschweiften Klammern künstlich angelegten Block wird noch eine Variable ups angelegt und bekommt den Wert 20.
Diese Variable hat einen "Block Scope", sie ist eine lokale Variable im Block und überlagert die gleichnamige Variable im globale Scope. Deshalb liefert der Konsole Aufruf im Block den Wert 20.
Am Ende, außerhalb des Blocks, gilt wieder die alte ups, die immer noch den Wert 10 enthält.
Dieser Mechanismus gilt für alle Blöcke, auch für Funktionen und Verzweigungen in IF Statements der Ähnlichem.
Scope Funktion und innerer Block
function f()
{
let ups = 30;
console.log("ups in Funktion:", ups);
if ( 1 == 1) {
let ups = 40;
console.log("ups im if Block:", ups);
}
console.log("ups in Funktion:", ups);
}
f();
console.log("ups global:", ups);
In diesem Code Ausschnitt wird die immer wiederkehrende Variable ups im globalen Scope angelegt, dann noch einmal im Rumpf der Funktion und noch einmal im Block der IF Verzweigung.
Jedes mal wird die im umschließenden Block existierende Version von ups durch die lokale Version ausgeblendet und die lokale Version wird aktuell.
Skripts und Scope
Wir hatten gesagt, dass der globale Scope an das Dokument gebunden ist. Das ist ernst zu nehmen. Eine anderes Verständnis von "globaler Scope" könnte sein, das er an das aktuelle Script gebunden ist. Das ist nicht der Fall.
Mehrere <script> Elemente in einer HTML Seite
let ups2 = "ups2 in einem Skript.";
</script>
<!-- Jede Menge HTML -->
<script>
console.log("ups2 im anderen Skript:", ups2);
</script>
Im zweiten Skript ist die Variable ups2 mit ihrem im ersten Skript zugeteilten Wert vorhanden.
Eine erneute Definition von ups2 im zweiten Skript, let ups2 = "Irgendwas";
, würde beim Laden der Seite zu einer Fehlermeldung führen.
Wenn mehrere Scripte in einem Dokument verwendet werden, so teilen sie sich den globalen Scope.
Operatoren
Vorweg
Es gibt viele Operatoren. Es gibt Operatoren, die aus einem Zeichen, aus mehreren Zeichen oder aus ganzen Wörtern bestehen. Es gibt Operatoren, die sich auf einen Ausdruck beziehen und welche, die zwei Ausdrücke benötigen. Es gibt Operatoren, die funktionieren bei allen Datentypen gleich. Es gibt aber auch welche, die funktionieren nur mit bestimmten Datentypen und es gibt welche, die funktionieren je nach Datentyp unterschiedlich. Operatoren können zu guter Letzt auch noch verkettet und geschachtelt werden. Alles in Allem ist es ein ziemliches Durcheinander.
In diesem Kurs werden nicht alle Operatoren vorgestellt, sondern nur die, die man häufig braucht.
Es gibt einen Operator, der dazu gedacht ist, bei der Programmierung Ordnung zu schaffen und die Übersicht zu behalten. Das ist der Operator ()
in der zweiten, unten beschriebenen Version. Von diesem Operator werden wir ausreichend Gebrauch machen und mit ihm fängt die Liste an.
Zugriff und Klammern
- ( ... ) normale Klammern
- 1. Ordnungsoperator.
In dieser Eigenschaft können runde Klammern fast überall stehen. Sie "klammern" Ausdrücke oder Bestandteile von Ausdrücken. Der Operator ist obligatorisch, wenn es im Bedingungen geht, aber auch sonst ist er hilfreich, um Klarheit in Ausdrücke zu bringen.
x = 4711 + "0815" - 3 + 1;
Was kommt am Ende heraus?
x = 4711 + "0815" - (3 + 1);
und hier?
x = 4711 + ("0815" - 3 + 1);
oder hier?
Alle drei Ausdrücke liefern komplett unterschiedliche Ergebnisse.
Die Regel für die Verwendung des Ordnungsoperators ist das genaue Gegenteil der Regel zum Überholen auf der Landstraße: Im Zweifel immer! - ( ... ) normale Klammern
- 2. Funktionsoperator. Die runden Klammern müssen hinter der Deklaration und auch beim Aufruf einer Funktion angegeben sein. Zwischen den Klammern können Parameter bzw. Argumente stehen.
- { ... } geschweifte Klammern
- 1. Objektoperator.
Die geschweiften Klammern werden zur Deklaration eines Objektes verwendet.
let o = { "a", 5, "hallo" };
Zwischen den geschweiften Klammern müssen die Mitglieder des Objektes gelistet werden. - { ... } geschweifte Klammern
- 2. Blockoperator.
In dieser Eigenschaft können die geschweiften Klammern dort stehen, wo eine Liste von Anweisungen zusammengefasst werden soll. Sie "klammern" Ausdrücke oder Bestandteile von Ausdrücken.
Die Klammern sind obligatorisch für die Definition eines Funktionskörpers und wenn Verzweigungen mehr als eine Anweisung umfassen.
function () { x = 1; y = 2; }
if ( ... ) { x = 1; y = 2; }
Der Blockoperator bildet einen eigenen Gültigkeitsbereich.
{ x = 1; y = 2; }
- [ ... ] eckige Klammern
- 1. Arrayoperator.
Mit den eckigen Klammern wird ein Array initialisiert. Zwischen den eckigen Klammern stehen die Elemente eines Arrays.
let a = [4711, "Hallo", "Tach"];
- [ ... ] eckige Klammern
- 2. Zugriffsoperator.
In dieser Eigenschaft stehen die eckigen Klammern direkt hinter dem Namen eines Arrays oder eines Objektes. Zwischen den Klammern steht der Index eines Arrayelements (
a[42]
) der Name eines Objektattributes (o["attrName"]
). Funktionen eines Objektes können so nicht aufgerufen werden.
- . der Punkt
- 1. Trennzeichen in Zahlen.
Bei der literalen Angabe einer Zahl ist der Punkt der Trenner zwischen Zahl und Nachkommastellen (
2.5
ist zweieinhalb). - . der Punkt
- 2. Zugriffsoperator.
Mit dem Punkt wird ein Attribut oder eine Funktion innerhalb eine Objektes angesprochen.
x = o.name;
o.name = "Müller";
x = o.berechne(1, 2);
Logische Operatoren
Logische Operatoren werden in Bedingungen eingesetzt.
Die logischen Operatoren verhalten sich in JavaScript genau so, wie in den meisten anderen Programmiersprachen.
- !
-
NOT. Negation einer Aussage. Was "wahr" ist wird "falsch" und was "falsch" ist wird "wahr".
Das "!" steht immer direkt vor dem Ausdruck, der negiert werden soll. Ist der Ausdruck komplizierter, muss geklammert werden. - &&
- AND. Logische "und" Verknüpfung. Wenn einer der Operanden falsch ist, ist die ganze Aussage falsch. Damit die ganze Aussage wahr wird, müssen alle Operanden wahr sein.
- ||
- OR. Logische - nicht exklusive - "oder" Verknüpfung. Wenn einer der Operanden wahr ist ist die ganze Aussage wahr. Damit die ganze Aussage falsch wird, müssen alle Operanden falsch sein.
Vergleiche
Die Vergleichsoperatoren in JavaScript entsprechen - bis auf den letzten - dem Standard.
- <, <=, >, >=
-
Die Operatoren "kleiner" und "kleiner gleich" und ihre Gegenteile brauchen für Zahlen keine Erklärung. Dasselbe gilt für ihre Gegenteile, "größer" und "größer gleich".
Bei Strings wird es komplizierter, weil Zeichensatz und Sprache eine Rolle spielen. Aber im Prinzip wird erst gegen das Alphabet und dann gegen die Länge geprüft. Also "aa" ist kleiner als "b" und "b" ist kleiner als "bb".
Boolean ("aa" < "b")
--> true
Boolean ("b" < "bb")
--> true
- ==, !=
-
"Lockere" Gleichheit. Die Operanden werden vor dem Vergleich umgewandelt. Gelingt das, so werden die umgewandelten Typen verglichen.
Boolean ("5" == 5)
--> true.
Boolean (null == false)
--> true.
Aber:
Boolean (null)
--> false. - ===, !==
-
"Strenge" Gleichheit. Die Operanden werden vor dem Vergleich nicht umgewandelt. Bei Operanden unterschiedlichen Typs, liefert der Vergleich immer false.
Boolean ("5" === 5)
--> false.
Boolean (null === false)
--> false.
Erwähnenswert ist die Vielseitigkeit des Gleichheitszeichens. Es gibt das Gleichheitszeichen in Verbindung mit dem Größer- und Kleinerzeichen, >=
und <=
, was "größer/gleich" und "kleiner/gleich" bedeutet, in Verbindung mit dem Ausrufezeichen, !=
, was "ungleich" bedeutet und es gibt das Gleichheitszeichen pur: Einfach, doppelt und dreifach, =
, ==
, ===
In der ersten Version ist es eine Zuweisung, in der Zweiten ein semantischer und in der dritten ein technischer Vergleichsoperator.
Vergleiche werden überwiegend in Bedingungen eingesetzt. Als Ausdruck kann man mit einem Vergleich an sich nur eine boolesche Variable laden (let x = 2 > 1;
).
Vergleichsoperatoren sind so nicht anwendbar auf Objekte!
(Siehe Exkurs: Objekte und Vergleiche)
Zwei Tipps:
Zweimal hinsehen, wenn einer der Operanden ein Objekt ist.
Dreimal hinsehen, wenn beide Operanden Objekte sind.
Zuweisungen
- =
- Das Gleichheitszeichen ist der generelle Zuweisungsoperator. Der Variable auf der linken Seite wird der Wert des Ausdrucks auf der rechten Seite zugewiesen.
- :
- Der Doppelpunkt ist der Zuweisungsoperator in Objekten bei der Initialisierung von Attributen. Das gilt allerdings nur bei einer literalen Konstruktion innerhalb der geschweiften Klammern. Außerhalb und in einem formalen Konstruktor ist es wieder das Gleichheitszeichen.
Berechnungen
Fangen wir mit dem lästigsten Operator an, dem ganz unscheinbaren Operator "+", das Pluszeichen.
- zahl1 + zahl2
- Der Plus Operator, angewendet auf Zahlen, führt eine mathematische Addition durch.
- string1 + string2
- Der Plus Operator, angewendet auf Strings, führt eine Verkettung durch. string2 wird an string1 hinten angehängt.
- zahl + string
- Der Plus Operator führt dazu, dass zahl in einen String umgewandelt wird. Danach werden die beiden Strings verkettet.
- +string
-
Der Plus Operator liefert, wenn er direkt vor einem String platziert ist, den Inhalt von string als Zahl. Die Variable string selber wird dabei nicht verändert. Kann der Inhalt von string auf keine Weise als Zahl interpretiert werden, liefert der Operator "NaN".
Der Operator kann verwendet werden, um in einer gemischten Operation mit Zahlen und Strings die mathematische Operation zu erzwingen. Der etwas merkwürdige Ausdruckzahl + +string
führt eine Addition durch, keine Verkettung.
Die restlichen mathematischen Operatoren sind in JavaScript gleich oder ähnlich implementiert, wie man es von anderen Programmiersprachen gewohnt ist.
Auch wenn die anderen Operatoren in ihrem Verhalten vergleichsweise berechenbar sind, muss man trotzdem aufpassen, wenn man mit einer Mixtur aus String und Number hantiert.
JavaScript wird immer versuchen, bei einer Operation mit gemixten Datentypen, einen String stillschweigend in eine Zahl umzuwandeln. Wenn das gelingt, wird munter (und stillschweigend) mit dem Ergebnis weiter gerechnet. Wenn es nicht gelingt, ist das Ergebnis "NaN" - auch stillschweigend. Erstmal. Bis es ganz woanders kracht.
.Es gibt einen rein mathematischen Operator, der hier besonders erwähnt werden soll, weil er in der Form in vielen anderen Programmiersprachen nicht implementiert ist.
- zahl1 ** zahl2
- Der Doppel-Sternchen Operator erhebt zahl1 in die Potenz, die mit zahl2 angegeben wird. "Zahl 1 hoch Zahl 2". Kurz, knackig, einfach.
Berechnung und Zuweisung
Oft sind Berechnungen mit Zuweisungen verbunden. Oder Zuweisungen mit Berechnungen. JavaScript bietet daher die ganze Liste an Kurzschreibweisen, die auch in C++ oder Java implementiert sind.
Die Liste soll hier nicht aufgezählt werden. Aber die Logik dahinter kann beschrieben werden.
Nehmen wir den Ausdruck
z = x + y;
Der lässt sich nicht weiter verkürzen. Es sind drei Variablen im Spiel (es wird auch dreimal ein Variablenname verwendet) verbunden mit zwei Operatoren.
Jetzt machen wir eine kleine Modifikation, und erhalten ein Szenario, das recht häufig vorkommt.
x = x + y;
Eine Variable x soll um einen bestimmten Betrag y erhöht werden. Jetzt sind an sich nur noch zwei Variablen im Spiel, obwohl wir drei aufschreiben. Das lässt sich verkürzen.
x += y;
Diese Kurzschreibweise ist mit allen mathematischen Operatoren möglich. Also auch mit -=
, *=
,/=
und sogar mit **=
.
Grundsätzlich sind diese Abkürzungen numerischen Daten vorbehalten. Allerdings funktioniert der +=
Operator auch für Strings. Damit lässt sich sehr komfortabel Text zusammensetzen.
Ganz zum Schluss kommt noch die bekannteste aller Kurzschreibweisen, der Operator ++
, bzw. --
.
Diese Operatoren sind für "Laufvariablen" gedacht. Für Fälle, in denen über eine bestimmten Zahlenbereich iteriert werden soll, indem eine Variable immer um eins hoch oder runter gesetzt wird.
Syntaktisch steht der Operator immer direkt vor oder direkt hinter einem Variablennamen. Die bekanntere Variante ist wohl x++
und nicht ++x
. Bei beiden Varianten kommt, was x betrifft, dasselbe heraus: x ist hinterher um eins größer als vorher.
Die folgenden Anweisungen sind gleichwertig:
x++;
++x;
x += 1;
x = x + 1;
Gleichwertig sind x++
und ++x
aber nur im Hinblick auf den Inhalt der Variabel x. Die Seiteneffekte sind unterschiedlich und als schwer zu findende Programmfehler äußerst beliebt. Das folgende kurze Skript macht das deutlich.
Die folgenden Anweisungen sind nicht gleichwertig:
let x, y;
x = 1;
y = x++;
console.log("x:", x, "y:", y);
x = 1;
y = ++x;
console.log("x:", x, "y:", y);
Bei der ersten Zuweisung (y = x++;
), wird der Wert von x an y zugewiesen, danach wird x inkrementiert, in der zweiten Version (y = ++x;
) wird erst x hochgezählt und danach die Zuweisung an y vorgenommen. Das ist offensichtlich nicht das Gleiche.
Kontrollstrukturen
Kontrollstrukturen zerfallen grob in zwei Sorten: Verzweigungen und Schleifen. Das zentrale "Dingsbums", worum sich beide Sorten ranken, sind Bedingungen.
Bedingungen
Vorweg
Eine Bedingung ist entweder wahr oder falsch, true
oder false
.
Psychologen, Philosophen, Künstler und Politiker werden sich die Haare raufen: Es gibt keinen Kompromiss, keinen Mittelweg, kein "sowohl als auch". Was nicht wahr ist, ist falsch. Was nicht falsch ist, ist wahr.
Eine Bedingung kann sehr einfach sein, sie kann aber auch beliebig kompliziert werden, wenn sie sich aus Teilbedingungen zusammen setzt.
Teilbedingungen werden durch logische Operatoren verknüpft.
Eine Bedingung kann Anweisungen und Funktionsaufrufe enthalten.
Die einzige Bedingung für eine Bedingung ist: Am Ende muss sie entweder wahr oder falsch ergeben.
Was ist falsch? Was ist wahr?
Am Einfachsten ist es, mit der Definition von "falsch" zu beginnen.
falsch (false)
Neben dem eigentlichen Wert false
gibt es andere Werte, die zu false
äquivalent sind. Äquivalent heißt, dass sie in einer Bedingung als false
interpretiert werden
Definition für "falsch" (false
)
Folgende Ausdrücke sind oder werden zu "falsch":
- Der boolesche Wert
false
. - Die Zahl
0
. - Der leere String
""
. - Eine Variable mit dem Wert
null
. - Eine undefinierte Variable (
undefined
). - Eine definierte Variable mit dem Wert
undefined
. - Ein Ausdruck mit dem Ergebnis
NaN
. (Gescheiterte Typumwandlung.)
wahr (true)
Die Definition von true
lässt sich jetzt sehr einfach und ein wenig stumpf aus false
ableiten.
Definition für "wahr" (true
)
Alles, was nicht falsch ist, ist wahr. (Interpretation für Kinder und die, die es bleiben wollen: "Alles, was nicht verboten ist, ist erlaubt.")
Natürlich gilt das auf der booleschen Ebene in beiden Richtungen. Allerdings ist die Ableitung von true
aus false
einfacher, weil die "falschen" Werte aufzählbar sind, die "wahren" nicht.
Beispiele
Einfache Bedingungen
Beispiele für einfache Bedingungen
if (false) // false
if (1) // true (gilt für jede Zahl != 0
if (0) // false
if (5 == 4) // false
if (5 != 4) // true
if (5 >= 4) // true
if (5 >= 5) // true
if (5 > 5) // false
Böse Sachen
if ("5" === 5) // false, keine Typenumwandlung
if (0) // false
if ("0") // true, nicht leere Zeichenkette
if ("") // false, leere Zeichenkette
if (" ") // true, nicht leere Zeichenkette
Kombinierte Bedingungen
Bedingungen können kombiniert und geschachtelt werden. Verbunden werden die einzelnen Teile mit den Operatoren "||" (UND), "&&" (ODER) und "!" (NICHT).
Kombinationen
if (1 == 1 || 1 == 1) // true
if (1 == 1 && 1 == 2) // false
if (1 == 1 || 1 == 2) // true
if (1 == 2 || 1 == 2) // false
if (!(1 == 1)) // false
if (!(1 != 1)) // true
if (!(1 == 2 && 1 == 1)) // true
if (!(1 == 2) && !(1 == 1)) // false
Bedingungen mit Ausdrücken und Funktionen
Bedingungen können auch ganze Ausdrücke enthalten oder Funktionen aufrufen.
Dabei muss man etwas aufpassen, da die Ausdrücke tatsächlich ausgeführt und die Funktionen aufgerufen werden.
Ausdrücke und Funktionen
if (f() == "Guten Morgen"); // true, wenn f() "Guten Morgen" sagt.
if (memberCount() > 0); // true, wenn memberCount positive Zahl zurück liefert
if ((x = memberCount()) > 0); // true, wenn x nach Zuweisung positiv ist
if (x = memberCount()); // true, wenn x nach Zuweisung nicht falsch ist
In dem Beispiel wird in der ersten Zeile der Variablen x ein neuer Wert zugewiesen. Und das unabhängig davon, ob die Bedingung am Ende war oder falsch ist: In beiden Fällen ist x hinterher 12.
Das Gleiche gilt für die Funktionen, die danach benutzt werden. Was auch immer die Funktion f() tut, um die Tageszeit herauszubekommen, es ist passiert.
Mehr böse Sachen
if (x = 0); // false, Zuweisung 0
if ((x - y) == 0); // true, wenn x == y
if (x - y); // false, wenn x == y
if ((x += y) == 0); // true, wenn x nach der Zuweisung 0 ergibt
if (x += y); // false, wenn x nach der Zuweisung 0 ergibt
if (x++ == 5); // false, wenn vorher x = 4
if (++x == 5); // true, wenn vorher x = 4
Exkurs: Objekte und Vergleiche
Der Vergleich von zwei Objekten funktioniert nicht. Oder, sagen wir, es ergeben sich dabei merkwürdige Ergebnisse, mit denen man in der Regel nichts anfangen kann.
1. Prüfung auf Gleichheit
Gleichheit von zwei Objekten
o2 = {a: "XX"};
console.log("o1 == o2:", o1 == o2); // false
o2 = o1;
console.log("o1 == o2:", o1 == o2); // true
Bei einem Vergleich von zwei Objekten mit "==" (oder "===") kommt immer false
heraus. Egal, was in den Objekten drin steht.
Der Vergleich liefert nur dann true
, wenn beide Variablen auf dasselbe Objekt verweisen, also identisch sind.
Dieses Verhalten gilt für alle Objekte, egal von wem oder auf welche Art sie angelegt wurden.
Dieses Verhalten gilt nur für Vergleiche, bei denen beide Operanden Objekte sind. Vergleicht man ein Objekt, das einen primitiven Datentyp abbildet, mit einem primitiven Datentyp, in den das Objekt umgewandelt werden kann, funktioniert es wieder.
2. Prüfung auf geordnete Ungleichheit (größer/kleiner)
Da normale Objekte prinzipiell nicht geordnet werden können, ist die Prüfung auf "<", ">", "<=" oder ">=" bei diesen Objekten nicht sinnvoll.
Nicht sinnvoll heißt, dass es keine vernünftige Regel für die Entscheidung gibt, wann ein Objekt größer oder kleiner als ein anderes ist. Wann ist eine Adresse größer als eine Andere? Wann ist ein Tisch, ein Schrank, ein Vogel im mathematischen Sinn größer oder kleiner als ein anderer? War Napoleon >
als Friedrich der Große? Oder eher <=
? Oder gar komplett <
?
Ungleichheit von zwei Objekten
o2 = {a: 100};
console.log("o1 >= o2:", o1 >= o2); // true
console.log("o1 <= o2:", o1 <= o2); // true
console.log("o1 > o2:", o1 > o2); // false
console.log("o1 < o2:", o1 < o2); // false
Bei einem Vergleich von zwei ungeordneten Objekten mit "<" oder ">" kommt immer false
heraus, egal, was in den Objekten drin steht.
Bei einem Vergleich von zwei ungeordneten Objekten mit "<=" oder ">=" kommt immer true
heraus, egal, was in den Objekten drin steht.
Es wäre an sich wünschenswert, wenn JavaScript in diesen Fällen "undefined" liefern würde.
Bei Objekten, die von JavaSript für primitive Datentypen angeboten werden, z.B. String
oder Number
, funktionieren die Größenvergleiche.
Verzweigungen
if-then-else
Die einfachste, bekannteste und bei weitem am Häufigsten eingesetzte Verzweigung ist "if ... else".
WENN Dies-Und-Jenes, mach Das-Und-Das, SONST mach Was-Anderes
Die Standardform
console.log ("x == y", true);
} else {
console.log ("x == y", false);
}
Dazu gibt es wenig zu sagen. Außer dass der else Zweig komplett weg gelassen werden kann.
If's können geschachtelt werden.
Verschachteltes if
console.log("x != y");
let r = 4711 * (17 + 4);
if (r > z) {
console.log("r > z");
} else {
console.log("r <= z");
}
} else {
console.log("x == y");
}
Wenn x ungleich y ist, wird eine komplizierte Berechnung durchgeführt und abhängig davon, ob das Ergebnis der Berechnung einen bestimmten Wert übersteigt, werden weitere Aktionen durchgeführt.
Manchmal werden if's geschachtelt, um eine besonders komplexe und unübersichtliche Gesamtbedingung aufzubrechen. Das Ergebnis ist eine Verschachtelung in Form eines "if Gebirges".
Die Topologie eines Gebirges in "if-Form" sieht so aus:
if Gebirge
if (x > z) {
if (x + y + z > 100) {
if (x == 30) {
}
}
}
}
Das Aufbrechen einer komplizierten Bedingung ist sicher ein Schritt in die richtige Richtung, aber wenn das innerste "if" am rechten Rand eines 30" Monitors klebt, sollte man die Logik noch einmal überdenken.
Ein weiteres Muster ist eine Abfolge von "if" oder "if .. else" Statements.
Einfache if Abfolge
console.log ("x <= 10", true, "x:", x);
}
if (x <= 20) {
console.log ("x <= 20", true, "x:", x);
}
if (x <= 30) {
console.log ("x <= 30", true, "x:", x);
}
if (x > 30) {
console.log ("x > 30", true, "x:", x);
}
Verkettung
console.log ("x <= 10", true, "x:", x);
} else if (x <= 20) {
console.log ("x <= 20", true, "x:", x);
} else if (x <= 30) {
console.log ("x <= 30", true, "x:", x);
} else {
console.log ("x > 30", "x:", x);
}
switch...case
Das switch...case
Statement greift die Problemstellung auf, die oben schon bei der Verkettung von if's zu sehen war: Es leitete eine Fallentscheidung ein.
Es ist wichtig zu sehen, dass als Vergleichsoperator bei einem switch
immer der Operator ===
(und nicht ==
) verwendet wird. Es findet also keine implizite Typumwandlung statt. Das kann auch nicht verändert werden. (Allerdings kann man das Verhalten austricksen.)
Die klassische Aufgabenstellung für ein switch
ist eine Fallentscheidung.
klassischer switch
case 0 :
console.log("x === 0");
break;
case 1:
console.log("x === 1");
break;
case 2:
console.log("x === 2");
break;
default:
console.log("Passt nicht: x =", x);
}
Die Variable x kann unterschiedlich numerische Werte annehmen. In dem Beispiel sind die Werte 0, 1, und 2 interessant und werden behandelt. Alle anderen Werte werden in dem default
Block aufgefangen.
Bemerkenswert ist:
- Im Kopf des
switch
wird die Variable (der Ausdruck) bestimmt, um den es geht. - Mit dem Schlüsselwort
case
werden die einzelnen Fälle bestimmt. - Der ":" beendet die Fallbeschreibung und leitet die Liste der Statements ein, die in dem Fall abzuarbeiten sind. Aber Achtung:
- Das Schlüsselwort
break
beendet die tatsächliche Abarbeitung von Statements. Fehlt dasbreak
so werden munter auch alle Statements abgearbeitet, die sich weiter unten in anderencase
befinden.
Das passiert so lange, bis einbreak
auftaucht oder das gesamteswitch
Statement zu Ende ist.
Mit dem switch
Statement kann man zaubern. Man kann in der Klammer nach dem switch
einen Ausdruck, z.B. einen Funktionsaufruf, platzieren. Das Gleiche kann man beim case
machen und vor dem Doppelpunkt eine Funktion aufrufen. Man kann an manchen Stellen ein break
setzen und an manchen nicht. Man kann auch das default
irgendwo in der Mitte platzieren.
Was man auch machen kann, ist in den Kopf des switch
das Schlüsselwort true
setzen. Dann kann man in den case
Bedingungen alles schreiben, was man auch bei einem if
als Bedingung nehmen könnte.
Kurz gesagt: Man kann ein switch
Statement schreiben, bei dem keiner weiß, was es eigentlich macht.
Einige Beispiele für "interessante" Verwendung von switch
mit Durchfall finden sich bei switch: Taking advantage of fall-through.
Das alles ist nicht Sinn der Sache. Der Sinn des switch
ist eine Spezialisierung des if
Statements für Fälle, bei denen es um die Auswertung des Inhalts einer Variablen geht. Eingesetzt wird es dann, wenn eine Variable (oder ein Ausdruck) unterschiedliche Werte annehmen kann, auf die jeweils eine unterschiedliche Reaktion erfolgen sollen.
Das break
wird in den Fällen weggelassen, wenn mehrere verschiedene Fälle auf dasselbe hinauslaufen.
Sinnvoller Einsatz von "fall trough"
case '?' :
case 'h' :
case 'H' :
console.log("Hilfe");
break;
case 'c':
console.log("kompilieren");
break;
case 'a':
case 'l':
console.log("veraltet");
break;
default:
console.log("Ubekannte Option ", option);
}
Schleifen
"while" Schleife
Die Mutter aller Schleifen
Die Standardschleife heißt while. Sie sieht so aus und verhält sich so, wie man es erwartet. Sie sieht in JavaScript so aus und verhält sich so, wie in anderen Programmiersprachen auch.
while
ist die Mutter aller Schleifen. Und sie ist total trivial.
Syntax der while Schleife
while (Bedingung) { MachWas }
while
Standardschleife
while (doIt) {
console.log("mach irgendwas im Loop");
let ergebnis = kommtVonDraussen();
if (ergebnis != null) {
console.log("mach irgendwas mit dem Ergebnis", ergebnis);
} else {
doIt = false;
}
}
Dieses - sinnlose - Beispiel verdeutlicht die einzelnen Schritte bei der Verwendung von while
.
Die Variable doIt steht für eine beliebige Bedingung. Sie wird außerhalb der Schleife deklariert und initialisiert. Damit die Schleife anläuft, muss sie am Anfang true
ergeben.
Innerhalb des Schleifenkörpers kann Beliebiges passieren: Statements, Berechnungen, Funktionsaufrufe. Es gibt aber etwas, das passieren muss:
Die Bedingung muss innerhalb der Schleife geändert werden.
Sonst kommt - vermutlich ungewollt - eine Endlosschleife dabei heraus.
Hier wird eine Funktion (kommtVonDraussen()) aufgerufen, deren Ergebnis entweder verarbeitet wird oder die Bedingung auf false
setzt.
--
Im Folgenden gibt es ein paar typische Muster, denen man immer wieder begegnet.
Als erstes wird unser Beispiel "kompaktiert".
while
Kompaktschleife
while ((ergebnis = kommtVonDraussen()) != null) {
console.log("mach irgendwas", ergebnis);
}
Das ergebnis ist in den Kopf gewandert, zusammen mit der Funktion, die es liefert. Da der Wert null
in der booleschen Logik zu false
führt, ist die Bestimmung "!= null" an sich überflüssig. Allerdings würde dann auch eine Rückgabe wie 0 oder ein leerer String ("") zum Abbruch führen. Da muss man wissen, was man will.
In dieser Form funktioniert die "Kompaktschleife" genau so, wie die "Standardschleife" darüber.
--
Was häufig zu sehen ist, weil man es häufig braucht, ist ein Zähler. Allerdings wird dafür i.d.R. ein for
Loop eingesetzt.
while
Zählschleife
while (x <= 100) {
console.log("x:", x);
++x;
}
Der Zähler wird außerhalb der Schleife initialisiert und innerhalb stumpf hochgezählt. Das passiert so lange, bis der Schwellwert überschritten ist.
Eine weiteres Muster ist die Endlosschleife. Sie wird selten eingesetzt. Sinn macht sie dann, wenn viele Abbruchbedingungen existieren, die sich erst im Körper der Schleife ergeben und deshalb im Kopf schwer aufzuschreiben sind.
while
Endlosschleife
if ((x * 50 / 13 + 8) == 42) break;
if ((x ** 2) > 30) break;
if (kommtVonDraussen() == "Guten Morgen") break;
console.log("x:", x);
++x;
}
Diese Schleife läuft solange, bis das Quadrat von x größer als 30 ist oder eine wenn eine merkwürdige Rechnung 42 ergibt oder eine Funktion "Guten Morgen" zurück liefert. Die Bedingungen haben nichts miteinander zu tun und lassen sich nur schlecht im Kopf der Schleife unterbringen.
do...while
Die do...while
Schleife gibt es in allen mir bekannten Programmiersprachen. Allerdings wird sie im Vergleich zum normalen while
recht selten benutzt.
Bei der do ... while
Schleife steht die Bedingung am Ende, was bedeutet, dass der Code in der Schleife immer mindestens einmal durchlaufen wird.
Syntax der do ... while
Schleife
do {
console.log("x:", x);
++x;
} while (x <= 2)
Diese Schleife wird 3 mal durchlaufen, bis x den Wert für die Abbruchbedingung erfüllt.
Besonderheit der do ... while
Schleife
do {
console.log("x:", x);
++x;
} while (x <= 2)
Diese Schleife wird einmal durchlaufen, obwohl x den Wert für die Abbruchbedingung schon vorher erfüllt.
"for" Schleife
Die for
Schleife wird mindestens so oft eingesetzt, wie die while
Schleife. Die while
Schleife ist unter dem Strich vielseitiger und in mehr Szenarien einsetzbar, aber for
zielt auf den häufigsten Anwendungsfall einer Schleife, die Iteration, und bildet diesen Fall komfortabel ab.
for
: Standardfall
console.log("in for x:", x);
}
Die Syntax ist auf den ersten Blick etwas sperrig. Aber wenn wir vom "ersten Blick" reden, dann haben wir eine Stärke der for
Schleife schon erfasst: Die gesamte Schleifenkontrolle steht im Kopf und ist auf den ersten Blick sichtbar.
Innerhalb der Klammer hinter dem for
stehen drei Ausdrücke:
- Die Initialisierung eines
for
Laufes. - Die Bedingung, unter der der Schleifenkörper ausgeführt werden soll.
- Die Bedingung, unter der der Schleifenkörper ausgeführt werden soll.
Die drei Ausdrücke müssen in dieser Reihenfolge dort stehen und sie müssen durch Semikolon getrennt sein.
Die Ausdrücke müssen nicht so einfach sein. Es können mehrere Variablen initialisiert werden, die Abbruchbedingung unterliegt den ganz normalen Regeln für eine Bedingung, kann also beliebig komplex werden und das Hochzählen muss keine Addition um 1 sein.
Alle drei Ausdrücke können sogar leer sein. Nur die Semikolons müssen da sein.
Nutzt man diese Möglichkeiten, so kann man richtig komplexe und wunderbar unübersichtliche for
Schleifen bauen.
Bleibt man bescheiden und nutzt for
für das, für das es gedacht ist, wird der Code kompakter und deutlich übersichtlicher.
Ein anderer Vorteil des for
gegenüber while
wird sichtbar, wenn man die Schleifen im Kontext des umgebenden Programms betrachtet: Die Laufvariablen des for
sind ohne viel Aufhebens im lokalen Scope und machen daher außerhalb keinen Ärger.
Das Besondere am for
Standardfall
for (let x = 0; x <= 2; ++x) {
console.log("in for x:", x);
}
console.log("nach for x:", x);
Hier gibt eine Variable x, die im globalen Scope definiert ist. Für den Ablauf der Schleife wird ein anderes x deklariert und initialisiert, das sich im lokalen Scope das Schleifenkörpers befindet und am Ende der Schleife wieder verschwunden ist.
Um dasselbe Verhalten mit while
zu implementieren sind mehr Zeilen und mehr Gedanken nötig.
Das while
Äquivalent zum for
: Standardfall
{
let x = 0;
while (x <= 2) {
console.log("in while x:", x);
++x;
}
}
console.log("nach while block x:", x);
Die Abkapselung der Laufvariablen x von ihrer Umgebung, kann mit while
nur erreicht werden, wenn man das ganze while
Konstrukt in einen separaten Block packt. Künstlich, holperig.
Es kommen im Folgenden ein paar Beispiele für den Einsatz von for
Loops, die man manchmal sieht. Nur in seltenen Fällen macht es wirklich Sinn, eine for
Schleife so zu verwenden.
for
: Unüblicher Gebrauch
console.log("in for x:", x);
}
Diese Schleife wird genau einmal durchlaufen.
for
: Ohne Initialisierung
console.log("in for x:", x);
}
Hier wird eine im äußeren Block gesetzte Variable x verwendet, mit was auch immer welchen Wert sie hatte.
for
: Ohne Abbruchbedingung
console.log("in for x:", x);
if (x > 2) break;
}
Die Abbruchbedingung muss "irgendwie" im Schleifenkörper ermittelt werden. Danach wird die Schleife mit break
verlassen.
for
: Ohne Berechnung
console.log("in for x:", x);
++x;
}
Die Neuberechnung der Variablen für die Abbruchbedingung muss innerhalb des Schleifenkörpers erfolgen.
for
: Ohne Initialisierung und Inkrement
for(; x <= 2;) {
console.log("in for x:", x);
x++;
}
Diese Form ist praktisch der Nachbau einer while
Schleife im Standardformat.
for
: Ohne Alles. Endlosschleife
console.log("in for x:", x);
++x;
if (x > 2) break;
}
Manche Leute bevorzugen diese Form, wenn sie eine Endlosschleife brauchen. Ob jetzt das prägnante for (;;)
oder das klassische while(true)
benutzt wird, ist Geschmackssache.
Beiden ist gemeinsam, dass die Kontrolle über die Schleife komplett im Schleifenkörper liegt.
"for...of" Schleife
Container Iteration
Die Iteration über die Elemente eines Containers ist ein besonderer und immer wiederkehrender Einsatz von Schleifen.
In JavaScript gibt es dafür spezielle Konstruktionen, die genau diese Art von Iteration unterstützen.
Unter Container verstehen wir Datentypen, die von ihrer Struktur her dafür ausgelegt sind, viel Daten sammeln und verwalten zu können.
Die im Standard-JavaScript vordefinieren Container sind:
- Array
- Map
- Set
Darüber hinaus gibt es in der HTML Schnittstelle noch weitere Container, die den Umgang mit Elementen und dem DOM unterstützen. Beispiele sind NodeList oder HTMLCollection.
Alle Container sind auf die eine oder andere Art von Object abgeleitet, daher ist ein Container auch ein Objekt. Das gilt aber nicht umgekehrt!
Immer, wenn man ein pures Object als Container benutzt, begibt man sich auf Glatteis und wird mit merkwürdigem Verhalten belohnt.
Container Iteratoren
Alle wirklichen Container in JavaScript implementieren drei Methoden - keys(), values(), entries() -, die den Inhalt des Containers in einer iterierbaren Form bereitstellen. Das Ergebnis kann mit dem Statement for...of durchlaufen werden. Genau dafür ist for...of gedacht.
Alle drei Funktionen sind für jeden Container leicht anders implementiert, um der Art des Container gerecht zu werden. Im Falle eines Set liefern alle drei Funktion beispielsweise dasselbe Ergebnis.
keys()
Die keys() Funktion liefert die Schlüssel in einem Container.
Im Falle eines Arrays sind das die Indexe. Im Falle einer Map sind es die Schlüssel. Bei einem Set Schlüssel und Wert identisch.
Für Arrays liefert keys() auch diejenigen Indexe zurück, die selber oder deren Wert undefined ist.
values()
Die values() Funktion liefert die Werte in einem Container.
Dabei handelt es sich um die Werte, die einem Schlüssel zugeordnet wurden.
Für Arrays liefert values() nur definierte Werte zurück. Die Anzahl der Werte kann also durchaus kleiner sein, als die Trefferliste von keys().
entries()
Die entries() Funktion liefert Schlüssel und Werte eines Containers als Datenpaare.
Das Datenpaar ist ein 2-stelliges Array, das an der Stelle 0 den Schlüssel und an der Stelle 1 den Wert enthält.
Für Arrays liefert entries() alle möglichen Wertepaare zurück. Wenn eine Position nicht besetzt ist, steht an der zweiten Stelle undefined. Die Anzahl der Werte ist gleich der Trefferliste von keys().
"for...in" Schleife
Eine ungewöhnliche Schleife
Die for..in Schleife ist deshalb ungewöhnlich, weil sie nicht über Daten iteriert, sondern über Eigenschaften - also Strukturmerkmale - von Objekten.
Sinnvoll ist sie nur auf JavaScript Objekte anwendbar. In diesem Fall iteriert eine for..in Schleife über alle zu dem Zeitpunkt zu einem Objekt gehörenden Eigenschaften und liefert sie zurück.
Nachlesen
Ein pures Object ist kein Container.
Ein Object ist ein strukturierter Datentyp, der sich aus weiteren Daten zusammensetzt und dessen Struktur durch seine Eigenschaften bestimmt wird.
Da es JavaScript erlaubt, einem Objekt Eigenschaften beliebig hinzuzufügen und wieder wegzunehmen, kann man Objekte auch als Container gebrauchen. (Und in manchen Fällen ist das ganz schön einfach und handlich.) Das liegt aber an der technischen Implementierung von Objekten in JavaScript und hat nichts damit zu tun, wofür Objekte gedacht sind. Es ist streng genommen kein "Gebrauch" sondern ein "Missbrauch".