Inhaltsverzeichnis
JavaScript
Diese Bändchen beschreibt kompakt und übersichtlich die Sprache des Web – JavaScript. Der erste Teil erklärt die Grundlagen und Sprachmerkmale. Danach wird objektorientierte Programmierung behandelt. Behandelt wird weiterhin alles bis zur Version ECMAScript 2020.
Zielgruppe
Diese Buchreihe wendet sich an Anfänger und an Webentwickler, die sich intensiver mit JavaScript auseinandersetzen möchten und weg wollen vom einfachen Copy und Paste irgendwelcher Skriptteile. Vielleicht wollen Sie sich auch mit der MEAN-Welt beschäftigen. MEAN steht für “MongoDb Express Angular Node” und bezeichnet eine komplett auf JavaScript basierendes Entwicklungs-Ökosystem. Aber auch als Grundlage zum Programmieren von Web Components ist dieses Buch perfekt. Ebenso als Fundament für TypeScript, der nächste logische Schritt nach ECMAScript.
Vielleicht sind Sie aber auch ein Webdesigner, der JavaScript als eine hervorragende Möglichkeit entdeckt hat, seine Webseiten mit dynamischen Elementen aufzuwerten. Dabei haben Sie mit Texten zu tun, mit Formularen, mit der Darstellung von Datenbankinhalten, also alles, was eine dynamische Website ausmacht. Dann wird Ihnen dieses Bändchen einen der Teilaspekte, nämlich das Erzeugen des HTML auf dem Server, in besonders übersichtlicher Form zeigen.
Auf alle Fälle habe ich mich bemüht, keine Voraussetzungen an den Leser zu stellen. Sie müssen kein Informatiker sein, keine Programmiersprache perfekt beherrschen, keine höhere Mathematik kennen. Egal in welchem Zusammenhang Sie auf JavaScript gestoßen sind, Sie werden diesen Text lesen können.
Wie Sie diesen Text lesen können
Ich will Ihnen nicht vorschreiben, wie Sie diesen Text lesen sollten. Beim ersten Entwurf der Struktur habe ich mehrere Varianten ausprobiert und dabei festgestellt, dass es die ideale Form nicht gibt. Wenn ich mich an den verschiedenen Anwendungsarten orientiere, zerfällt der Text in mehrere Kapitel, die nicht im Zusammenhang miteinander stehen. Der eine oder andere Leser würde sich dann ärgern, dass er viel Geld für ein Buch ausgibt, das nur zu einem Fünftel verwendbar ist. Diese Bändchen löst das Problem, indem es auf ein sehr kleines Thema fokussiert ist und kein “bla-bla” zur Aufblähung des Umfangs dabei ist.
Anfänger sollten den Text als Erzählung lesen, von der ersten bis zur letzten Seite. Wer sich schon etwas auskennt, kann die für ihn weniger interessanten Abschnitte gefahrlos überspringen. Falls Bezüge notwendig sind, habe ich entsprechende Querverweise eingefügt.
Schreibweisen
Das Thema ist satztechnisch nicht einfach zu beherrschen, denn Skripte sind oft umfangreich und es wäre schön, wenn man die beste Leseform optisch unterstützen könnte. Ich habe deshalb an einige Stellen zusätzliche Zeilenumbrüche benutzt, die der Lesbarkeit dienen, im Editor Ihrer Entwicklungsumgebung aber nichts zu suchen haben.
Generell wird jeder Programmcode mit einer nicht proportionalen Schrift gesetzt. Außerdem verfügen Skripte über Zeilennummern:
Oft werde ich die Verwendung bestimmter Zeichen in einem solchen Ausdruck genau erläutern. Dann werden die “wichtigen” Zeichen durch Zeilenumbrüchen alleingestellt und auch in diesem Fall werden Zeilennummern dazu dienen, das betroffene Symbol im Text exakt zu referenzieren (Beachten Sie undefined
-Zeichen in Zeile 3):
Symbole
Um die Orientierung bei der Suche nach einer Lösung zu erleichtern, gibt es einige Symbole, die im Text genutzt werden.
Online-Unterstützung
JavaScript lernen können Sie mit jedem Browser. Aber manchmal ist es lästig, von Hand HTML, JavaScript und Hilfsdateien zusammenzubauen. Dann helfen Online-Editoren, sogenannte REPL-Umgebungen. Hier eine Auswahl:
-
- JustRun.It (http://justrun.it)
- Unterstützt auch React und Angular, ideal für die nächsten Schritte. Integrierte Konsole.
-
- JsFiddle (https://jsfiddle.net/)
- Kompakt, nur drei Dateien (HTML, CSS, JS). Viele Bibliotheken und Versionen. Online speichern und bereitstellen.
-
- Plunker (http://plnkr.co/)
- Für größere Projekte mit vielen JS-Dateien. Herunterladen als ZIP.
Über den Autor
Jörg arbeitet als Trainer, Berater und Softwareentwickler für große Unternehmen weltweit. Bauen Sie auf die Erfahrung aus 25 Jahren Arbeit mit Web-Umgebungen und vielen, vielen großen und kleinen Projekten.
Ihm vor allem solide Grundlagen wichtig. Statt immer dem neuesten Framework hinterher zu rennen wären viele Entwickler besser beraten, sich eine robuste Grundlage zu schaffen. Wer dies kompakt und schnell lernen will ist hier richtig. Auf seiner Website https://www.joergkrause.de sind viele weitere Informationen zu finden.
Er hat über 60 Titel bei renommierten Fachverlagen in Deutsch und Englisch verfasst, darunter einige Bestseller.
Kontakt zum Autor
Neben der Website können Sie auch direkten Kontakt über www.IT-Visions.de aufnehmen. Wenn Sie für Ihr Unternehmen eine professionelle Beratung zu Web-Themen oder eine Weiterbildungsveranstaltung für Softwareentwickler planen, kontaktieren Sie Jörg über seine Website oder buchen Sie direkt über http://www.IT-Visions.de.
1 Grundlagen
Dieses Buch beschreibt die Grundlagen und Entwurfsmuster von JavaScript. Dies ist eine kompakte Einführung, die dabei helfen soll, Beispiele zu Umgebungen wie Node.js oder diverse JavaScript-Frameworks zu verstehen und eigene Entwicklungen umzusetzen. Das Kapitel verzichtet auf umfassende geschichtliche Ausflüge zugunsten relevanter Inhalte. Behandelt wird hier der grundlegende Sprachumfang von ECMAScript bis Version ES2020.
1.1 Einführung
JavaScript ist eine relativ kompakte und mächtige Skriptsprache, welche um das Jahr 1995 von Netscape eingeführt wurde, um Webseiten mit Interaktionen ausstatten zu können. Seitdem sind 25 Jahre vergangen und JavaScript ist aus so gut wie keiner Webanwendung mehr weg zu denken. Dabei spielt es keine Rolle, mit welcher Technologie diese Webanwendung realisiert wurde. Je nach Hersteller des Browsers kann die Sprachimplementierung leicht variieren. Die häufigste Implementierung ist jedoch in Browsern zu finden, wo die Sprache die Grundlage für die interaktiven Elemente von HTML 5 liefert. Grundsätzlich ist JavaScript aber unabhängig vom Browser. Die Benutzung in Node ist eine vom Browser vollständig entkoppelte Implementierung der Sprache basierend auf der V8-Engine von Google, die auch den Browser Chrome antreibt.
Trotz des sehr ähnlich klingenden Namens und der teilweisen syntaktischen Ähnlichkeit zu JAVA ist JavaScript eher nicht mit der Programmiersprache JAVA verwandt. Bis auf die Ausnahme, dass beide Programmiersprachen sich in ihrer Syntax sehr an der Programmiersprache C orientiert haben.
Ursprünglich war JavaScript ausschließlich zu dem Zweck gedacht, den DOM-Baum (Document Object Model) des Browsers zur Laufzeit zu manipulieren. Jedoch entwickelten sich mit der Zeit immer mehr Anwendungsgebiete für diese universelle Skriptsprache. So gibt es bereits erste Referenzimplementierungen ganzer Anwendungsprogramme in JavaScript, beispielsweise basierend auf nw.js.
Laufzeiten
JavaScript wird von mehreren Laufzeitumgebungen unterstützt. In jedem Browser gibt es eine, aber mit verschiedenen Implementierungen und auf dem Server als Node.js. Es gibt einige weitere Versuche, JavaScript-Engines zu etablieren, aber keine konnte sich am Markt durchsetzen. Der neueste Versuch, Deno, hat bislang nur wenig Resonanz gefunden. Microsofts Alternative Chakra wurde wieder abgekündigt.
1.2 Sprachmerkmale
JavaScript als Programmiersprache wurde von einem der Erfinder folgendermaßen beschrieben:
Was ist das Problem mit JavaScript? Einige Zitate (im Original) helfen beim Verständnis:
- JavaScript was a initially introduced in Netscape 2.0B3 in Dec 1995, a.k.a. Mocha, LiveScript, Jscript, however, it’s official name is ECMAScript
- JavaScript is a C-family, world’s worst named, extremely powerful language (not a script), totally unrelated to Java
- JavaScript is a weakly typed, classless, prototype based OO language, that can also be used outside the browser. It is not a browser DOM.
- The world’s most misunderstood programming language. Douglas Crockford
Vergleich mit andern Sprachen
Die Kernideen von JavaScript sind:
- Eine funktionale Programmiersprache mit C-ähnlichen Syntax (
{}
und;
) - Dynamisch typisiert (Loose typing)
- Sichtbereiche (scope) funktionsorientiert
- Objektorientiert, aber klassenlos
- Auf Prototypen basierende Vererbung
Die Sprachelemente umfassen:
- Werte / Literale
- Variablen
- Operatoren
- Kontrollstrukturen
- Funktionen
- Objekte
- Typen
1.3 JavaScript-Syntax
Die Namen für Variablen und Funktionen beginnen mit einem Buchstaben, _
oder $
(In “Regex-Speak”: [a-zA-Z_$]), gefolgt von keinem oder mehreren Buchstaben, Zahlen, _
oder $
. Das $
hat keine spezielle Bedeutung, es sollte generiertem oder Bibliotheks-Code vorbehalten bleiben. Alle Variablen, Parameter oder Mitglieder werden klein geschrieben (meinVariablenName). Einzige Ausnahme: Konstruktoren beginnen mit einem großen Buchstaben (MeinKlassenKonstruktor). Das _
-Zeichen (Unterstrich) am Beginn eines Schlüsselwortes wird für private Mitglieder verwendet (privat per Konvention).
Einbetten in HTML
Das einfachste Beispiel für Browser (Listing 1.1), “Hallo JavaScript”, zeigt die Nutzung auf einer einfachen HTML-Seite:
JavaScript-Code kann an den unterschiedlichsten Stellen einer Webseite verwendet werden. Die zwei gebräuchlichsten davon sind der Kopfbereich (Zeile 4) als Codeblock, welcher meistens für die Definition von Funktionen verwendet wird, sowie innerhalb des Body einer Seite am Ende (Zeile 12ff.).
Grundsätzlich wird der auszuführende Code von einem entsprechenden Script-Tag eingeschlossen. Seit HTML 5 ist der Typ text/javascript als Mime-Typ für den Inhalt freiwillig, weil dies der Standardwert ist, der angenommen wird, wenn keine Angabe erfolgt.
Kommentare
Ein kommentierter Bereich sieht folgendermaßen aus:
Der Zeilenkommentar steht alleine auf einer Zeile oder am Ende:
Literale
Tabelle 1.1. zeigt einige Literale für Sonderzeichen. Daraus lässt sich ableiten, dass Sie den umgedrehten Schrägstrich (Backslash) in Zeichenfolgenausdrücken maskieren müssen (\\
).
Zeichen | Bedeutung |
---|---|
\b | BackSpace |
\n | NewLine |
\t | Tab |
\f | FormFeed |
\r | CarriageReturn |
Ein Beispiel dazu:
Umgang mit Umlauten
Eine Besonderheit ist beim Umgang mit Umlauten zu beachten. Es ist nicht sichergestellt, dass die Ausgabe mit den Sonderzeichen umgehen kann (in der Praxis heutzutage schon, aber ein Test ist sinnvoll). Aus diesem Grund ist es im Zweifelsfall empfehlenswert, auf die Funktion unescape
zurückzugreifen. Eine Ausgabe mit Umlauten sieht möglicherweise folgendermaßen aus:
Schreiben Sie jedoch folgendes, wenn Umlaute benutzt werden und die Fähigkeiten der Zielumgebung unklar sind:
Damit stellen Sie sicher, dass die Umlaute immer korrekt interpretiert und angezeigt werden können. Die Codierung ist das URL-Encoding, bei dem Sonderzeichen durch den hexadezimalen Wert des ASCII-Zeichens ersetzt werden, maskiert mit den %
-Zeichen.
URL-Encoding kann ein wenig konfus sein, weil keine Angabe des Zeichensatzes erfolgt. URL-Zeichen verlangen Oktets, also 7 Bit, und ASCII hat einen Umfang von 8 Bit. Alle 127 Zeichen im oberen Block müssen also encoded werden. JavaScript benutzt für Zeichen aber den erweiterten Satz UCS-2, der auch asiatische Schriftzeichen und tausende Symbole enthält. Das hat aber nichts mit URL-Encoding zu tun. Die benutzte JavaScript-Funktion unescape
(und das Pendant escape
) verhält sich deshalb bei Werten bis 255 (0x100) konform zum URL-Encoding (%HH) und darüber konform zum Encoding nach UTF-8 (%HHHH). UTF-8 ist das Encoding-Verfahren (welche Bits stellen welches Zeichen dar) für den Zeichensatz UCS-2 (welche Zeichen gibt es überhaupt, egal wie codiert).
Numerische und Boolesche Literale
Ferner gibt es noch die numerischen Literale. JavaScript unterscheidet im Wesentlichen in Ganzzahlen und Gleitkommazahlen bei den Literalen. Intern werden nur Gleitkommazahlen abgebildet. Ganzzahlen können in folgenden Formen vorkommen:
- Hexadezimale Konstanten: beginnen mit einem 0x vor der Zahl, gefolgt von den Zahlen 1…9, 0 oder den Buchstaben a..f, beispielsweise 0x12affe.
- Dezimale Ganzzahlenkonstanten: Beispielsweise 4711, also nur die Ziffern ohne Präfix.
Für binäre Zahlen (b) und oktale Zahlen (o) gibt es zwei weitere Literale. Diese Möglichkeiten sind neu in ECMAScript 2015.
Gleitkommazahlen können in zwei Schreibweisen vorkommen:
- Einfache Schreibweise: mit Punkt (.) getrennte Ziffern, beispielsweise: 12.56
- Exponentialschreibweise: mit Punkt (.) getrennte Ziffern mit einem Exponenten, beispielsweise 12.56e2
Ferner gibt es zur Darstellung von Wahrheitswerten noch boolesche Literale:
-
Wahr:
true
-
Falsch:
false
Sonstige Literale
Reguläre Ausdrücke werden in /
(slash) geschrieben. Das Array-Literal []
wird für Arrays benutzt. Das Objekt-Literal {}
erzeugt ein neues, leeres Objekt. Mehr dazu in den entsprechenden Abschnitten, die diese Typen behandeln.
Operatoren
Das Zeichen +
dient sowohl der Addition als auch der Verknüpfung von Zeichenketten. Wenn beide Operanden Zahlen sind, werden diese addiert, sonst erfolgt immer eine Umwandlung in Zeichenketten, welche zusammengesetzt werden.
Operator | Bedeutung | Beispiel |
---|---|---|
+ , +=
|
Addition | x+=3 |
- , -=
|
Subtraktion | x=x-5 |
* ,*=
|
Multiplikation | a=b*c |
/ , /=
|
Division | z=e/5 |
% |
Modulus | m=5 % 3 |
++ , --
|
Inkrement, Dekrement | x++ oder y- - |
<< , <<=
|
Bitweise Linksschieben | x << 4 |
>> , >>=
|
Bitweise Rechtsschieben | y >> 5 |
>>> |
Bitweise Linksschieben mit Nullfüllung | a >>> b |
& |
Bitweise UND | a & b |
| |
Bitweise ODER | a | b |
^ |
Bitweise Negieren | ^b |
1.4 Native Typen
JavaScript ist “schwach typisiert” (loosely typed). Keineswegs kann man hier von einer untypisierten Sprache sprechen. Wird einer Eigenschaft oder einer Variablen ein Wert zugewiesen, so bekommt die Variable den Typ des jeweiligen Wertes. Intern werden Typen unterschieden. Elementare Typen sind:
-
Number
: Zahlen -
String
: Zeichenketten -
Boolean
: Logische Werte -
Function
: Funktion (auch wenn als Klasse definiert) -
Symbol
: Global eindeutige Symbolbezeichner -
Object
: Alles andere
Dazu kommen die folgenden speziellen Zustände, die Variablen annehmen können:
null
undefined
Diese werden weiter unten noch genauer vorgestellt.
Globale Objekte
Des Weiteren sind einige Typen eingebaut, um alltägliche Aufgaben zu erleichtern. Dies sind technisch globale Objekte mit statischen Eigenschaften und Methoden. Von einige lassen sich jedoch auch Instanzen anlegen. Die wichtigsten sind:
-
Math
: Berechnungen -
NaN
,Infinity
: Not a Number, dazu mehr weiter unten -
BigInt
: Große Zahlen -
Date
: Datum und Zeit -
RegExp
: Reguläre Ausdrücke (Suchmuster) -
JSON
: Umgang mit JavaScript Object Notation
Diese Typen basieren alle auf Object
. Für den Umgang mit Daten sind folgende Typen geeignet:
-
Array
: Allgemeine Arrays. Dazu gibt es spezialisierte Formen:Float32Array
Float64Array
BigInt64Array
BigUint64Array
Int16Array
Int32Array
Int8Array
TypedArray
Uint16Array
Uint32Array
Uint8Array
Uint8ClampedArray
SharedArrayBuffer, Atomics
Map
Set
WeakMap
WeakSet
-
ArrayBuffer
,DataView
- Fehlerobjekte:
-
Error
,EvalError
,InternalError
,RangeError
,ReferenceError
,SyntaxError
,TypeError
,URIError
-
-
Intl
: Globalisierungsunterstützung - Generator-Funktionen:
AsyncIterator
GeneratorFunction
Generator
Iterator
-
Promise
: Asynchrone Rückruffunktionen -
Proxy
: Stellvertreterobjekte mit Zugriffsbenachrichtigung -
Reflect
: Informationen über Objektinterna -
WebAssembly
: Zugriff auf Web-Assembly-Code
Automatische Typkonvertierung
Wird die Konvertierung nicht explizit angegeben, versucht JavaScript den am besten passendsten Typ zu ermitteln. Durch verschiedene Operatoren können Umwandlungen elegant erzwungen werden. Dazu mehr im Abschnitt Operatoren.
Number
Number
ist der einzige Zahlentyp, ein 64Bit Gleitkomma (floating point).
Nach IEEE-754 ist der Zahlentyp in JavaScript double
.
Ein Spezialwert für Zahlen ist NaN
– Not a Number. Dieser entsteht als Ergebnis fehlerhafter oder unmöglicher Berechnungen. Jede Berechnung mit einem Parameter NaN
ergibt NaN
. NaN
ist ungleich zu allem, auch zu NaN
selbst. Der folgende Vergleich ergibt immer false
:
Testen kann man auf diesen Wert mit der folgenden statischen Methode:
Number.isNaN(value)
Die Klasse Number
bietet einige solche explizite Funktionen. Eine Instanz wird folgendermaßen erstellt:
Dies konvertiert einen Wert in eine Zahl. Im Fehlerfall entsteht NaN
. Konvertiert werden Zahlen besser durch Operatoren. Mit + sieht das dann folgendermaßen aus:
Das Plus-Zeichen wird hier als nicht verändernder Präfix-Operator benutzt. Damit erzwingt man die Umwandlung in eine Zahl.
Alternativ gibt es noch die globale Funktion isNaN()
(ohne Number
davor). Diese prüft nur, ohne vorherige Konvertierung. Zusammenfassend gilt:
-
isNaN()
: Prüfe und gib das Ergebnis zurück. -
Number.isNaN()
: Mach eine Zahl draus und prüfe dann, was dabei herausgekommen ist.
Das ist oft gleich, aber eben nicht immer.
Die Funktion parseInt(value, radix)
verwandelt in eine Ganzzahl mit einer definierten Zahlenbasis (bei Dezimalzahlen ist die Basis 10). Im Fehlerfall entsteht wieder NaN
:
Falls eine Gleitkommazahl als Eingabe erwartet wird, eignet sich parseFloat(value)
`, hier natürlich ohne Zahlenbasis:
Bei Berechnungen kann der Wert Infinity
entstehen. Die Funktion zum Testen heißt isFinite()
. Das ist bemerkenswert, denn man würde als Test eigentlich so etwas was wie isInfinite erwarten, in Analogie zu NaN
. Noch bemerkenswerter ist in diesem Zusammenhang, dass das Objekt Number
zwei statische Eigenschaften besitzt, Number.POSITIVE_INFINITY
und Number.NEGATIVE_INFINITY
. Hier wird also strenger nach mathematischen Regeln unterschieden. Zum Testen gibt es wieder Number.isFinite()
, was analog zum vorherigen Beispiel erst konvertiert und dann testet, während die global Funktion lediglich eine bestehende Tatsache feststellt.
Manche scheinbar einfache Zahlen lassen sich nicht im bestehenden Zahlenraum abbilden. Es gilt beispielsweise folgendes:
Das Problem ist die Zahl 0,2, die im binären Zahlenraum nicht abbildbar ist. Abbildung 1.1 zeigt die Herleitung der dezimalen Zahlen aus den binären. Bei 0.2 müsste für eine präzise Darstellung der Rest weitergeführt werden, ws aber endlos ist.
Irgendwann ist die Mantisse erschöpft und damit ist die Genauigkeit begrenzt. Die Anzahl der signifikanten Stellen ist dabei entscheidend.
- 0.000000234 = 2.34 × 10<sup>−7</sup>: 3 signifikante Stellen: 234
- 1.000000234 = 1.000000234 × 100: 10 signifikante Stellen: 1000000234
Dadurch, das im ersten Beispiel der Exponent benutzt wird, kann eine viel höhere Genauigkeit erreicht werden.
Das gilt auch bei großen Zahlen.
Das wird der Zahlenwert richtig verändert. Das kann fatale Folgen haben. Der Grund ist, dass der Zahlenraum die Zahl 9007199254740993 nicht kennt und die nächstbeste binäre Darstellung bei der Umwandlung nach Dezimal eine andere Zahl ergibt. Der Zahlenraum bis 2<sup>53</sup> gilt als sicher. Die folgende Funktion udn zwei Eigenschaften können zum Testen benutzt werden, ob Sie sich noch im sicheren Bereich bewegen.
Außerhalb sollte entweder eine passende Bibliothek benutzt werden oder die Berechnungen werden entsprechend modifiziert.
Berechnungen mit Math
Das Math
-Objekt enthält Konstanten und Funktionen zur Berechnung. Es können auch komplexe Berechnungen wissenschaftlicher oder kaufmännischer Art durchgeführt werden. Die Sprachspezifikation sollte als Referenz zusätzlich benutzt werden.
Eine Instanz von Math
braucht nicht eigens erzeugt werden. Eigenschaften und Methoden von Math
sind statisch und können direkt verwendet werden. Das Schema sieht folgendermaßen aus (Pseudocode mit Platzhaltern):
Mit z = 10 * Math.PI
beispielsweise steht in der Variablen z nach der Zuweisung das Produkt aus der Zahl π und 10. Mit w = Math.sqrt(10)
steht in der Variablen w hinterher das Ergebnis der Quadratwurzel aus 10. Vor jedem Aufruf einer Eigenschaft oder Methode des Math-Objekts wird Math
benutzt (großgeschrieben).
Bei jedem Zahlen-Parameter, den Sie einer Methode von Math
übergeben, kann es sich um eine explizite Zahl (beispielsweise 25 oder 0.123) handeln, um eine numerische Variable (beispielsweise x) oder um einen Rechenausdruck (beispielsweise 7 * 5 + 0.3). Auch Rechenausdrücke mit Variablen sind erlaubt (beispielsweise x * i + 1).
Alle typischen trigonometrischen Funktionen steht zur Verfügung. Diese werden mit dem Bogenmaß durchgeführt oder geben das Ergebnis in Bogenmaß zurück. Um vom Bogenmaß in das Winkelmaß umzurechnen, dividieren Sie diesen einfach durch (Math.PI / 180
). Eine Multiplikation mit diesem Wert sorgt für die Umrechnung von Grad- in Bogenmaß.
Die trigonometrischen Funktionen sind:
-
acos()
(Arcuscosinus) -
asin()
(Arcussinus) -
atan()
(Arcustangens) -
cos()
(Cosinus) -
sin()
(Sinus) -
tan()
(Tangens)
Der hauptsächliche Einsatzzweck sind Animationen und Effekte. Auch zum Zeichnen von Kreisen sind sin und cos gut geeignet.
Die folgende Funktion (Listing 1.2) zeichnet einen Viertelkreis:
Das dazu passende HTML-Element sieht folgendermaßen aus:
Weitere Funktionen
Einige weitere typische Funktionen sind:
-
abs()
: absoluter (positiver) Wert einer Zahl -
ceil()
: die nächsthöhere ganze Zahl -
exp()
: Exponentialwert -
floor()
: die nächstniedrigere ganze Zahl -
log()
: der natürlichen Logarithmus -
max()
: die größere zweier Zahlen -
min()
: die kleinere zweier Zahlen -
pow()
: Potenz -
random()
: Auswahl zwischen 0 bis 1 per Zufall -
round()
: kaufmännische Rundung einer Zahl auf die nächste Ganzzahl -
sqrt()
: die Quadratwurzel
Wichtig sind in der Praxis oft ceil
und manchmal floor
. Ein typisches Beispiel ist die Ermittlung der Anzahl der Seiten beim Blättern durch Datensätze. Wenn man 83 Elemente in Gruppen zu zehn anzeigt, benötigt man neun Seiten.
Die Variable pages hat nun den Wert 9.
Die Rundungsfunktion hat keine Option, auf eine bestimmte Anzahl von Stellen zu runden. Sie rundet immer auf ganze Zahlen. Listing 1.3 zeigt eine sinnvolle Funktion dafür (mit einer Standardrundung auf 2 Stellen):
Konstanten
Die wichtigsten Konstanten lauten:
-
E
: Eulersche Konstante -
LN2
: natürlicher Logarithmus von 2 -
LN10
: natürlicher Logarithmus von 10 -
LOG2E
: konstanter Logarithmus von 2 -
LOG10E
: konstanter Logarithmus von 10 -
PI
: Konstante PI -
SQRT1_2
: Konstante für Quadratwurzel aus 0,5 -
SQRT2
: Konstante für Quadratwurzel aus 2
Die Zahlenwerte in der gezeigten Reihenfolge und mit der erreichbaren Genauigkeit zeigt Abbildung 1.4:
Zeichenkette (String)
Ein Zeichenkette ist eine Sequenz von 0 oder mehr 16Bit-Zeichen. In ECMAScript wurde ursprünglich UCS-2 benutzt, wobei einige Implementierungen UTF-16 benutzen. Die Ausgabe sollte immer in UCS-2 erfolgen, intern wird meist UTF-16 eingesetzt. Der Unterschied macht sich bemerkbar, wenn die Anzahl der Bytes ermittelt wird, aus denen ein Zeichen besteht.
Es gibt keinen speziellen Typ für ein Zeichen (kein char-Typ). Ein Zeichen wird durch einen String der Länge 1 repräsentiert. Zeichenketten sind unveränderlich (immutable), wie in den meisten anderen Sprachen auch. Zeichenketten gleichen Inhalts sind gleich (==), auch wenn es sich um verschiedene Objekte handelt. Dies entspricht der intuitiven Lesbarkeit.
Zeichenkettenliterale können "
oder '
verwenden. Die Benutzung ist gleichwertig. Dadurch sind Verschachtelungen möglich wie "Hallo 'Welt' "
oder 'Hallo "Welt"'
.
Die Länge der Zeichenkette wird mit string.length
ermittelt. String(value)
wandelt einen Wert in eine Zeichenkettendarstellung um.
Vorlagenzeichenfolgen
Vorlagenzeichenfolgen (template strings) sind eine syntaktische Vereinfachung zum Bauen von Zeichenketten mit variablem Anteil. Sie benutzen ein neues Zeichenkettenliteral, das Backtick “`”.
Der Aufruf von String.raw
mit der unmittelbar anschließenden Zeichenkette ohne Klammern (Zeile 14 in Listing 1.4) ist kein Fehler. Dies sind markierte (tagged) Vorlagen. Jede Funktion kann so aufgerufen werden. Es findet eine Konvertierung in ein Array mit den statischen Teilen und den Argumenten als nachgestellte Parameter statt:
Die Ausgabe sieht dann folgendermaßen aus:
Die Tag-Funktionen, die benutzt werden, sind völlig frei wählbar. Sie müssen auch keinen Wert zurückgeben. Meist erscheint dies jedoch sinnvoll.
Die String-Methoden
Zeichenketten in JavaScript sind 0-basiert, das heißt das erste Zeichen hat den Index 0. Dies gilt für alle Zeichenkettenmethoden, die mit Indizes arbeiten. Zeichenketten sind wie bereits erwähnt unveränderlich (immutable). Methoden, die Änderungen vornehmen, geben immer eine neue Instanz einer Zeichenkette zurück. Mehrfach Manipulationen sind dadurch zwar sehr schnell, führen aber auch dazu, dass dieselben Zeichen möglicherweise mehrfach im Speicher liegen. Bei großen Zeichenketten sollten Sie sehr sorgfältig mit den Datenmengen umgehen.
indexOf()
- Mit dieser Methode wird die Position einer Zeichenkette in einer anderen Zeichenkette gefunden. Wird nichts gefunden, wird -1 zurückgegeben.
lastIndexOf()
- Wenn mehrere Vorkommen einer Zeichenkette in einer anderen existieren, wird die die letzte Fundstelle zurückgegeben. Wird nichts gefunden, wird -1 zurückgegeben.
search()
- Mit dieser Methode wird die Position einer Zeichenkette in einer anderen Zeichenkette gefunden. Wird nichts gefunden, wird -1 zurückgegeben. Im Gegensatz zu
indexOf
akzeptiertsearch
auch reguläre Ausdrücke als Argument.
Drei Methoden dienen dazu, Teile von Zeichenketten zu bilden:
slice(start, end)
- Extrahiert einen Teil einer Zeichenkette von Position start bis Position end. Wenn das Argument negativ ist, wird die Position vom Ende gezählt.
substring(start, end)
- Extrahiert einen Teil einer Zeichenkette von Position start bis Position end. Negative Argumente sind nicht erlaubt. Wird das Ende end weggelassen, wird bis zum Ende der Zeichenkette extrahiert.
substr(start, length)
- Extrahiert einen Teil einer Zeichenkette von Position start und dann eine Anzahl Zeichen length.
Die Ausgabe ist in beiden Fällen ‘Banane’.
Die Ausgabe ist auch in diesen beiden Fällen ‘Banane’.
replace(search, replace)
- Diese Methode ersetzt Teile einer Zeichenkette durch eine andere:
toUpperCase
- Wandelt alle Zeichen in Großbuchstaben um.
toLowerCase
- Wandelt alle Zeichen in Kleinbuchstaben um.
concat()
- Diese Methode kann mehrere Argumente annehmen und kombiniert diese zu einer Zeichenkette.
Manipulationen einzelner Zeichen vermeiden den Umgang mit großen Zeichenketten in manchen Fällen.
-
charAt(position)
,charCodeAt(position)
- Die Methoden geben das Zeichen an der gewählten Position zurück.
charCodeAt
gibt dabei den Unicode des Zeichens an.
Da JavaScript keinen expliziten Zeichentyp kennt, ist die Interpretation einer Zeichenkette als Array unzulässig. Es ist zwar syntaktisch möglich, kann aber unvorhersehbare Fehler produzieren. Schreibweisen wie str[0]
gelten deshalb als sogenanntes Antipattern. Sollten Arrayfunktionen sinnvoll erscheinen, konvertieren Sie die Zeichenkette zuerst explizit in ein Array-Objekt. Am einfachsten geht das mit der Methode split
.
Beachten Sie die Funktion des Trennzeichens. Wenn kein Trennzeichen angegeben wird, steht die gesamte Zeichenkette im Index 0 des Arrays. Um eine Zeichenkette zeichenweise zu zerlegen und dabei keine expliziten Trennstellen zu definieren, nutzen Sie eine leere Zeichenkette ""
.
Nicht standardkonforme String-Methoden
Einige Methoden sind nicht vom Standard abgedeckt, werden aber von praktisch allen Browsern unterstützt. Umgebungen außerhalb des Browsers, wie beispielsweise Node.js, unterstützen diese Funktionen nicht.
Methode | Erzeugt das HTML-Element… |
---|---|
anchor(name) | <a name="name"></a> |
big() | <big></big> |
blink() | <blink></blink> |
bold() | <b></b> |
fixed() | <tt></tt> |
fontcolor(c) | <font color="c"></font> |
fontsize(s) | <font size="s"></font> |
italics() | <i></i> |
link(href) | <a href="href"></a> |
small() | <small></small> |
strike() | <strike></strike> |
sub() | <sub></sub> |
sup() | <sup></sup> |
Der Inhalt des Elements ist jeweils die Zeichenkette, auf die die Methode angewendet wurde.
Um direkt zuzugreifen, benötigt man eine String-Instanz oder den String-Prototypen, der die Grundlage der Instanz ist. Mit Prototype sieht es folgendermaßen aus:
Eine String-Instanz ist nicht effektiver, aber effektiver zu beschaffen:
Boolean
Boolesche Werte können true
(Wahr, Ja) oder false
(Unwahr, Nein) sein. Beides sind Literale in JavaScript.
Die Konstruktorfunktion Boolean(value)
ermittelt, ob der Wert true
oder false
ist. Werte, die als false
interpretiert werden, sind:
false
null
undefined
-
""
(leere Zeichenkette) 0
NaN
Alle anderen Werte sind true
, einschließlich "0"
und "false"
(in Anführungszeichen), was manchmal zu Verwirrung führt.
Bei automatischen Typumwandlungen, beispielsweise bei Benutzung mit der Anweisung if
, kommen dieselben Regeln zur Anwendung.
1.5 Objekte
Objekte in JavaScript sind dynamisch. Dies ist eine Mischung aus einem Objekt und einem Dictionary (Map). Mit new Object()
erzeugen Sie einen leeren Container für Name/Wert-Paare. Alternativ kann auch das Literal für ein Objekt {}
verwendet werden. Der Name kann jede Zeichenkette sein, der Wert darf alles außer undefined
sein. Auf Mitglieder kann über die Dot-Notation (Objekt.Member) oder die Index-Notation (subscript) zugegriffen werden. Die Dot-Notation ist nur zulässig, wenn der Name des Mitglieds ein gültiger Bezeichner ist. Er muss dazu mit einem Buchstaben, dem Unterstrich oder dem $-Zeichen beginnen und darf keine Leerzeichen enthalten.
Folgende Definition ist möglich:
Neben der Punktschreibweise kann noch die Index-Schreibweise benutzt werden. Auch hier handelt es sich nur um eine Eigenschaft (Zeile 2):
Das ist insofern bemerkenswert, weil dadurch auch Namen benutzt werden können, die andernfalls syntaktisch unzulässig wären, also mit Leer- oder Sonderzeichen.
Spezielle Typen
Speziell ist der Umgang mit nicht definierten Variablen. Prinzipiell ist keine Deklaration nötig, weil der Typ erst bei der Benutzung festgelegt wird. Deshalb wird häufig der Zustand explizit abgefragt. Ein Variable kann undefiniert sein, das heißt dann undefined
. Sie kann auch existieren, aber völlig leer sein. Das ist dann null
.
null versus undefined
null
ist ein Wert, welcher “Nichts” repräsentiert. Es ist ein Objekt mit dem Typ null
. undefined
ist dagegen kein Wert, er zeigt eine undefinierte Variable oder einen fehlenden Rückgabewert an. Es ist kein Objekt, eben gar nichts. Der Standard für nicht definierte Variablen und nicht vorhandene Member ist undefined
.
Die Abfrage von null
ist relativ häufig und dies immer mit if
zu tun ist aufwändig. Deshalb gibt es den Null-Operator ??
.
Ist die Variable links tatsächlich null
oder undefined
, wird der Ausdruck rechts ausgewertet.
Beim Verketten von Ausdrücken ist ein ähnlicher Operator mit derselben Funktion einsetzbar:
Statt einer Ausnahme wird hier einfach undefined
zurückgegeben. Durch die Verkettung kann erheblich Code eingespart werden.
1.6 Symbole
Symbole erzeugen eindeutige Token. Sie können als Namen benutzt werden und sind robuster als Zeichenketten.
Das Erzeugen erfolgt immer mit der Symbol
-Fabrikfunktion, nicht mit dem Operator new
.
Die Zeichenfolge ist optional und dient nur der Dokumentation. Deshalb gilt:
Prüfen Sie den Typ mit typeof
, wird symbol zurückgegeben. Es handelt sich also um einen nativen Typ.
Der typische Einsatzzweck sind eindeutige Bezeichner wie im folgenden Beispiel:
Absolute Eindeutigkeit
Auf den ersten Blick mag, vor allem im Hinblick auf das letzte Beispiel, eine Zeichenkettenkonstante ausreichend sein. Schauen Sie sich dazu folgenden Code an:
Zeile 9 zeigt, dass der Zugriff gelingt, indem eine Zeichenkette benutzt wird anstatt der Konstanten. Das passiert wegen einer Unachtsamkeit, aus Unkenntnis oder absichtlich zur Umgehung von Programmierpfaden. Es gibt viele “Gründe”, warum ein Entwickler undiszipliniert ist. Wird nun die Konstante umbenannt, verlässt sich derjenige auf die Wirkung der Umbenennung. Die Zeichenfolge wird davon nicht erfasst, ein Programmierfehler entsteht und möglicherweise sehr weit von der Stelle entfernt, wo die Änderung vorgenommen wurde.
Symbole üben hier einen gewissen Zwang aus, der die Benutzung von Zeichenketten verhindert oder zumindest deren Benutzung zuverlässig erkennt.
Wir nun eine Zeichenkette benutzt, oder auch nur ein anderes Symbol, wirft der Code zuverlässig eine Ausnahme. Die Benutzung der Konstanten wird durch die Symbole effektiv erzwungen.
1.7 Arrays
Arrays sind ebenfalls Objekte. Sie können diese mit dem Konstruktor Array
oder dem []
-Literal anlegen:
- Konstruktor mit Daten:
new Array(elm1, elm2, elm3, …)
- Konstruktor ohne Daten:
new Array(length)
- Literal:
[elm1, elm2, elm3]
Einige Methoden für Arrays
Es gibt Methoden, die ein existierendes Array verändern:
push(e)
- Fügt ein Element am Ende ein und gibt die neue Länge zurück.
pop()
- Entfernt das Element am Ende und gibt es zurück.
reverse()
- Dreht die Reihenfolge der Elemente im Array um.
shift()
- Entfernt das Element am Anfang und gibt es zurück.
sort()
- Sortiert das Array und gibt das neue Array zurück.
splice(start, entfernen, neu...)
- Entfernt Elemente und fügt neue ein. Die zu entfernenden werden ab start gezählt und die Anzahl mit entfernen angegeben. Alle weiteren Parameter werden als neue Elemente ab start eingefügt.
unshift(neu...)
- Fügt Elemente am Anfang ein und gibt die neue Länge zurück.
Es gibt weiterhin solche Funktionen, die etwas zurückgeben, jedoch am Array selbst keine Änderungen vornehmen:
join(sep)
- Verbindet alle Elemente eines Arrays zu einer Zeichenkette. Als Trennzeichen – welches optional ist – wird das oder werden die Zeichen sep benutzt.
slice(start, ende)
- Extrahiert den Teil eines Arrays von start bis ende.
concat
- Verbindet Arrays zu einem neuen Array.
indexOf(s)
- Index der ersten Fundstelle der Zeichen s oder -1, falls nichts gefunden wurde.
lastIndexOf
- Index der letzten Fundstelle der Zeichen s oder -1, falls nichts gefunden wurde.
Methode, die auf Iteratoren aufsetzen, eignen sich zum Durchlaufen von Arrays:
forEach(callback, this)
- Ruft eine Funktion callback für jedes Element des Arrays auf. Der Parameter this kann benutzt werden, um der Funktion den Wert für
this
vorzugeben. every(callback, this)
- Gibt
true
zurück, wenn jedes Element eines Arrays einer in der Rückruffunktion benannten Bedingung gehorcht. some(callback, this)
- Gibt
true
zurück, wenn mindestens ein Element eines Arrays einer in der Rückruffunktion benannten Bedingung gehorcht. filter(callback, this)
- Gibt ´die Elemente zurück, die der in der Rückruffunktion benannten Bedingung gehorchen.
map(callback, this)
- Gibt die Elemente zurück, die die Rückruffunktion für jedes Element zurückgibt.
reduce(callback, init)
- Reduziert ein Array, indem alle Werte gelesen werden. Der vorherige Wert wird mittels Rückruffunktion weitergereicht, sodass man addieren, subtrahieren oder sonstwas mit den Werten anstellen kann. Am Ende wird ein skalarer Wert zurückgegeben. Die Funktion beginnt links, am Index 0.
reduceRight(callback, init)
- Reduziert ein Array, indem alle Werte gelesen werden. Der vorherige Wert wird mittels Rückruffunktion weitergereicht, sodass man addieren, subtrahieren oder sonstwas mit den Werten anstellen kann. Am Ende wird ein skalarer Wert zurückgegeben. Die Funktion beginnt rechts, am letzten Index.
1.8 Operatoren
Die Operatoren in JavaScript sind unspektakulär. Es ist eher der Einsatz, der einige interessant macht.
Operatoren für Objekte
Es gibt einige spezielle Operatoren, die vor allem dem Umgang mit Objekten dienen:
-
new
: Erstellt eine Instanz eines Objekts -
instanceof
: Prüft ob ein Objekt eine Instanz eines Typs ist -
typeof
: Gibt den Typ eines Objekts zurück -
in
: Testet, ob ein Objekt eine Eigenschaft enthält -
delete
: Löscht eine Instanz eines Objekts
Im Kapitel zu Objekten folgen dazu weitere Informationen. An dieser Stelle nur der Hinweis, das new
tatsächlich ein Operator ist und eine Aktion auf einer Funktion ausführt.
Zu delete
ist anzumerken, dass JavaScript als verwaltete Sprache einen Garbage Collector besitzt und verwaiste Variablen selbst entsorgt. Normalerweise wird der Aufruf nicht benötigt. Will man Objekte aber vorher oder vorsorglich loswerden, ist der Einsatz sinnvoll.
Spread-Operator
Etwas speziell ist der Spread-Operator ...
. Er dient der Expansion von Objekten in Variablen oder in Array-Elemente. Angenommen eine Funktion erwartet einzelne Argumente, aber Sie haben ein Array. Dann geht folgendes:
In Zeile 3 wird der Spread-Operator dem Wert vorangestellt, um die Auflösung zu erzwingen. Das gilt auch, wenn Elemente eines Arrays erreicht werden sollen:
Die Variable parts wird erst in einzelne Elemente zerlegt und dann werden diese eingefügt.
Vergleichsoperatoren
Die logischen Vergleichsoperatoren sind folgende:
-
==
: Vergleich auf Wert-Gleichheit -
!=
: Vergleich auf Wert-Ungleichheit -
===
: Vergleich auf Wert- und Typ-Gleichheit -
!==
: Vergleich auf Wert- und Typ-Ungleichheit -
&&
: Logisches “und” -
||
: Logisches “oder”
Guard
&&
ist das logische UND, aber auch der guard-Operator. Wenn der erste Operand true
ergibt, dann ist das Ergebnis der zweite Operand, sonst wird der erste Operand zurückgegeben. Das können Sie nutzen, um null
-Referenzen zu vermeiden. Ohne Operator würde dafür ein if
zum Einsatz kommen:
Die folgende Kurzschreibweise mit guard-Operator ist weitaus einprägsamer:
Der Nullkettenoperator, der mit ES2020 eingeführt wurde, vereinfacht diesen Aufruf nochmals:
Beachten Sie hier, dass nicht alle Umgebungen dies bereits unterstützen. Die Einführung erfolgte in folgenden Versionen:
- Chrome 80
- Edge mit Chromium 80
- Firefox 74
- Safari 13.1
- Node.js 14
Das heißt, der alte Edge und IE können das nicht. Es gibt Polyfills oder die Option, Babel oder TypeScript zu nutzen.
Default
||
ist das logische ODER und auch der default-Operator. Wenn der erste Operand true
ergibt, dann ist das Resultat der erste Operand, sonst ist das Resultat der zweite Operand. Dies können Sie verwenden, um Standardwerte zu setzen. Ohne Operator würde dafür ein if
zum Einsatz kommen:
Die Kurzschreibweise mit default-Operator ist weitaus einprägsamer:
Das !
-Zeichen ist das logische Nein (not). Wenn der Operand true
ist oder ergibt, ist das Resultat false
. Sonst ist das Resultat immer true
. Trickreich ist !!
, das benutzt werden kann, um einen Ausdruck in ein Boolean zu konvertieren:
Dieser Ausdruck ergibt true
.
1.9 Anweisungen – Statements
Folgende Anweisungen stehen als Schlüsselwörter zur Verfügung:
-
if
,else
switch
-
while
,do
-
for
,for in
,for of
break
continue
return
try/catch/throw
function
-
var
,let
,const
Kontrollstrukturen: if – else
Die if
-Anweisung ist eine einfache Fallunterscheidung. Der if
-Ausdruck muss vom Typ Boolean sein oder er wird in ein Boolean umgewandelt. Der else
-Teil kann entfallen.
Kontrollstrukturen: switch – else – default
Der Befehl switch
dient der Fallunterscheidung. Als Ausdruck im switch
und den case
-Marken können Number
, String
und andere Typen gleichzeitig verwendet werden. Die case
-Marken sollten von einem break
beendet werden. Wird ein explizites case
nicht gefunden, wird der default
-Block angesprungen. Der default
-Block kann entfallen.
Kontrollstrukturen: Schleifen
Die Schleifen in JavaScript nutzen die Schlüsselwörter for
, while
und do
-while
.
while
Eine while
-Schleife wird, wie auch die for
-Schleife, so lange ausgeführt wie die Bedingung wahr (true
) ist. Die Bedingung wird vor dem Schleifendurchlauf getestet. Es kann also passieren, dass der Inhalt der Schleife nie erreicht wird.
do
Alternativ zur while
-Schleife kann die do
-Schleife verwendet werden, wenn die Bedingung am Ende eines jeden Schleifendurchlaufs geprüft werden soll. Wenn die Bedingung nicht zutrifft wird die Schleife dennoch mindestens einmal durchlaufen.
for
Für Schleifen, welche im weitesten Sinne einer Aufzählung entsprechen, kommt die for
-Schleife zum Einsatz, welche aus einem Anfangswert, einer Bedingung und einem Iterator besteht.
Solange die Bedingung wahr (true
) ist, wird die Ausführung der Schleife fortgesetzt. Dabei ist zu beachten, dass wenn die Bedingung beim Betreten bereits falsch (false
) ist, die Anweisungen innerhalb der Schleife nicht durchlaufen werden. Nach jedem Schleifendurchlauf wird die Iterationsanweisung ausgeführt und die Bedingung erneut geprüft.
Allgemein sieht das folgendermaßen aus:
Ein praktisches Beispiel zeigt das folgende Listing 1.6.
Zunächst wird die Variable i deklariert und mit dem Wert 1 initialisiert. Bitte beachten Sie, dass hier die Verwendung des Schlüsselwortes var
nicht erforderlich ist (Scope ist immer lokal).
In der Bedingung wird überprüft, ob i kleiner 11 ist, das bedeutet, die Schleife wird 10 Mal durchlaufen. In der Iterationsanweisung wird i mit Hilfe des unären Inkrement-Operators ++
um Eins erhöht. An dieser Stelle können Sie auch i+=1
oder i=i+1
verwenden, um gegebenenfalls andere Schrittweiten vorzugeben.
Eine Variante der for
-Schleife kann verwendet werden, um die Eigenschaften eines Objekts zu durchlaufen: die for in
-Schleife (hier mit Ausgabe ins Dokument):
Das Beispiel zeigt, wie die Eigenschaften des Objekts document
durchlaufen werden können, um beispielsweise die Fähigkeiten des jeweiligen Browsers zu ermitteln.
Schleifenkontrolle
Mitunter kommt es vor, dass eine Schleife vorzeitig abgebrochen werden soll. Hierfür wird das Schlüsselwort break
verwendet. Mit Hilfe des Schlüsselwortes continue
ist es möglich, die Abarbeitung mit dem nächsten Schleifendurchlauf fortzusetzen und alle folgenden Anweisungen im Anweisungsblock auszulassen.
Wenn i gleich 5 ist, wird die Abarbeitung der Schleife bei 6 fortgesetzt und alle weiteren Anweisungen werden übersprungen. Wenn i gleich 9 ist, wird die Schleife verlassen und keine weiteren Durchläufe ausgeführt. break
und continue
können sowohl für for
als auch für while
- und do
-Schleifen verwendet werden.
Verschachteln von Schleifen
Um bei geschachtelten Schleifen genauer festlegen zu können, welche der Schleifen mit einem continue
fortgesetzt wird, wird auf eine Marke (label) mit einem Namen und einem Doppelpunkt verwiesen.
Den Schleifen wurden jeweils die Label inner und outer zugeordnet, um später bei der Verwendung gezielt die innere oder die äußere Schleife ansprechen zu können. So wird für alle j gleich 3 die innere Schleife fortgesetzt und für alle j gleich 6 die äußere Schleife.
Schleifen mit for..in
Diese Anweisung dient dazu, ein Objekt zu durchlaufen. Objekte sind intern Maps, also Gebilde, die eine Struktur mit Schlüssel-/Werte-Paaren enthalten. Diese Schleife durchläuft alle Mitglieder eines Objekts. Dabei spielt es keine Rolle, ob es sich um Eigenschaften oder Methoden handelt. Dies ist oft nicht genau das, was gesucht wird und deshalb wird häufiger die neue Version for..of
benutzt.
Schleifen mit for..of
Die Iteratoren in ES2015 ermöglichen es, eigene aufzählbare Objekte zu erstellen. Dazu ist nicht unbedingt ein Array erforderlich. Zum Durchlaufen gibt es die neue Schleifenform for..of
.
Der Code nutzt eine bestimmte Objektstruktur, die vorgegeben ist. Deklariert muss sie nicht werden. Würde man denselben Code in TypeScript erstellen, könnten die Typ-Beschreibungen folgendermaßen aussehen:
In ES2015 ist es nicht möglich, derartige Typ-Beschreibungen zu hinterlegen. Die Darstellung dient nur der Erläuterung des inneren Verhaltens.
Polyfill erforderlich
Derzeit wird diese Technik nicht überall unterstützt. Setzen Sie gegebenenfalls ein Polyfill ein – eine Bibliothek, die bei Bedarf die fehlenden Funktionen nachbildet.
Fehlerbehandlung
Ausnahmen sind Fehlerbedingungen, welche sich nicht ohne weiteres vorhersehen lassen. Eine Ausnahme wäre beispielsweise zu erwarten, wenn versucht wird, etwas auf eine Festplatte zu schreiben, diese jedoch voll ist.
Ausnahmen (Exceptions)
Folgende Schlüsselwörter stehen zur Verfügung:
-
try
: Abgesicherter Block -
catch
: Fanganweisung -
finally
: Unbedingt ausgeführter Anteil (mit oder ohne Fehler) -
throw
: Weiterleiten oder Werfen einer Ausnahme
Im Falle eines Fehlers kann eine Ausnahme geworfen werden. Vereinfacht ausgedrückt wird mit dem “Werfen” einer Ausnahme der Programmfluss unterbrochen und an der Stelle fortgesetzt, wo diese Ausnahme mit einem try
/catch
-Block abgefangen wurde. Grundsätzlich kann jedes Objekt geworfen werden, dabei spielt es keine Rolle, ob Sie eine Zeichenkette oder ein selbstdefiniertes Objekt verwenden. Wichtig ist das Fangen einer Ausnahme.
Um eine benutzerdefinierte Ausnahme werfen zu können, ist es erforderlich, eine entsprechende Instanz zu erzeugen. In diesem Beispiel heißt diese ZeroDivError. Die Objektorientierung unter JavaScript wird unter Abschnitt Objektorientierung noch genauer betrachtet werden. Mit Hilfe des this
-Zeigers werden Eigenschaften wie der Name name und die Nachricht msg festgelegt. Soll keine Nachricht angegeben werden, kommt eine Standardnachricht zur Anwendung. Die Methode toString
wird erstellt. Immer wenn ein Objekt in eine Zeichenkette zu wandeln ist, wird diese Methode automatisch aufgerufen. In der Funktion div wird überprüft, ob der zweite Parameter für die Division null
ist. Für den Fall, dass der Parameter b==0
ist, wird eine entsprechende Ausnahme erzeugt und geworfen.
Mögliche Programmteile, welche eine Ausnahme werfen könnten, werden mit einem try
-Block umschlossen, welcher immer von einem catch
-Block gefolgt wird. Tritt jetzt eine Ausnahme auf, wird die Abarbeitung in dem try
-Block abgebrochen und in dem catch
-Block fortgesetzt. Ferner wird die geworfene Ausnahme innerhalb des catch
-Blocks zur weiteren Verarbeitung bereitgestellt.
Optional kann der catch
-Block noch von einem finally
-Block gefolgt werden. Unabhängig davon, ob eine Ausnahme aufgetreten ist oder nicht, wird der finally
-Block immer als letzter Block durchlaufen.
Standardausnahmeobjekte
Folgende Fehlerklassen sind für die Benutzung mit throw
eingebaut:
Error
EvalError
InternalError
RangeError
ReferenceError
SyntaxError
TypeError
URIError
Den Namenskonventionen folgend sollte man eigene Fehlerklassen auch mit dem Suffix Error belegen.
1.10 Variablen und Scope
Globale Variablen können an jeder Stelle durch Zuweisen eines Wertes angelegt werden. Lokale Variablen werden innerhalb einer Funktion mit Hilfe des Schlüsselwortes var
angelegt. Initiale Werte können Sie sofort angeben. Mehrere Variablen können durch Komma getrennt werden.
Plattform Global
Variablen sind intern Eigenschaften eines globalen Objekts. Dies ist abhängig von der gewählten Plattform. Normalerweise läuft die Suche nach einer Variablen im Scope ab und damit bei globalem Scope in das globale Objekt. Soweit es sich um selbst definierte Variablen handelt, ist das das natürliche Verhalten und muss nicht weiter beachtet werden. Wenn es aber um den Zugriff auf globale Eigenschaften geht, die gesucht werden, muss man den Namen des globalen Objekts kennen:
-
window
: Browser -
global
: Node.js -
self
: Web Worker
Skripte zu schreiben, der plattformunabhängig sind, kann deshalb aufwändig sein. Ab ES2020 steht deshalb ein neues Objekt zur Verfügung: globalThis
. Wenn also die Spezifizierung des globalen Objekts notwendig ist, benutzen Sie immer dieses.
Abgesehen davon ist es immer besser mit einem eingeschränkten Sichtbarkeitbereich zu arbeiten. Dies erklärt der nächste Abschnitt.
Scope – der Sichtbarkeitbereich
In ES 5 hatten Blöcke (in {}
) keinen eigenen Scope. Nur Funktionen hatten einen Scope. Variablendefinitionen innerhalb einer Funktion sind außerhalb der Funktion nicht sichtbar – sie sind im Funktions-Scope (auch als Gültigkeitsbereich bezeichnet).
Dieses Verhalten wird mit var
erzeugt. Wird es benutzt, ist die Variable lokal im Scope einer Funktion. Gibt es keine Funktion, ist die Variable global. Weitere Schutzebenen existieren nicht.
Variablen mit let und const
Die Schlüsselwörter let
und const
vereinbaren Variablen, nutzen aber einen anderen Scope als bei der Verwendung von var
. Statt der Bekanntmachung innerhalb einer Funktion wird die Variable nun für einen Block deklariert. ECMAScript 2015 verfügt also über einen Block-Scope. Der Scope entsteht durch ein Paar geschweifte Klammern.
Die Werte, die mit const
zugewiesen wurden, sind überdies unveränderlich. Ansonsten verhält sich const
wie let
. Beachten Sie allerdings, dass const
keinen Schutz für Eigenschaften eines Objekts beinhaltet. Objektmitglieder lassen sich weiter ändern.
Das return-Schlüsselwort
Mit return
weisen Sie das Verlassen der aktuelle Funktion an. Es kann mit oder ohne Rückgabewert verwendet werden:
Ohne Ausdruck gibt return
den Wert undefined
zurück. Die Ausnahme hier: Konstruktoren geben die Instanz des Objekts zurück. Dazu finden Sie mehr im Kapitel zu objektorientierter Programmierung.
2 Objektorientierung
JavaScript verfügt über eine sehr einfache Unterstützung für objektorientierte Programmierung. Es wird nicht explizit von Klassen gesprochen, da sich die abstrakte Definition eines Objekts nur sehr gering von der konkreten Implementierung einer Funktion unterscheidet. JavaScript-Objekte verfügen über Eigenschaften (Attribute) und Funktionen (Methoden).
2.1 Erstellen von Objekten
Im Wesentlichen gibt es drei Möglichkeiten, ein Objekt in JavaScript zu erstellen. Im Folgenden sollen diese kurz vorgestellt werden.
Deklaration mit new
Die einfachste Methode Objekte zu erstellen, ist die Verwendung des Operators new
. Dies ist eine Referenz an die Sprache Java, tatsächlich verhält sich new
etwas anders in JavaScript, denn in Java ist es ein Schlüsselwort, kein Operator.
Ein benutzerdefiniertes Objekt person wird erstellt. Dem Objekt werden zwei Eigenschaften zugewiesen (name und height). Ferner wird eine Mitgliedsfunktion run erstellt, welche wiederum zwei neue Eigenschaften dem Objekt hinzufügt. Statt der Schreibweise new Object()
kann auch das Objektliteral {}
benutzt werden. Wegen seiner Kürze wird es meist bevorzugt.
Deklaration mit der Literalnotation
Die Literalnotation ist eine implizite Schreibweise.
Zunächst wird myObject angelegt und die Eigenschaften property1 bis property3 werden mit Werten belegt (Zeile 2 bis 4). Dann wird eine Mitgliedsfunktion method1 (Zeile 6) deklariert. Folgende Ausgabe ergibt sich hier:
JSON
Beachten Sie, dass gültiges JSON (JavaScript Object Notation), so wie es viele Dienste im Web erzeugen, ein gültiges Objektformat in JavaScript ist. Umgekehrt gilt dies nicht, von allen in JavaScript syntaktisch gültigen Schreibweisen eines Objekts sind nur wenige Kombinationen gültiges JSON.
Tatsächlich wird JSON auch mit anderen Sprachen benutzt und ist trotz des unglücklich gewählten Namens keinesfalls eng mit JavaScript verbunden.
JSON wird explizit durch das Objekt JSON
unterstützt, mit dem Zeichenketten geparst oder erzeugt (serialisiert) werden können. Nur zwei statische Methoden existieren:
-
JSON.parse()
: Parst eine Zeichenketten und erzeugt ein Objekt. Ein weiteres Argument liefert eine optionale Steuerfunktion. -
JSON.stringify()
: Serialisiert ein Objekt in eine Zeichenkette. Es gibt zwei weitere optionale Argumente, die den Vorgang steuern und das benutzte Leerzeichen bestimmen.
Der Konstruktor
Die bisher gezeigten Varianten ein Objekt anzulegen sind etwas eingeschränkt, da keine Möglichkeit besteht, mehrere Instanzen des gleichen Objekts anzulegen. Ferner ist es auch nicht möglich, beim Anlegen Parameter für die Initialisierung des Objekts mitzugeben.
Klassische Konstruktorfunktionen
Aus diesem Grund soll an diese Stelle die dritte Variante für das Erstellen eines Objekts mittels Prototyping gezeigt werden. Zunächst wird ein abstraktes Objekt deklariert und anschließend wird dieses mit einem konkreten Parameter instanziiert.
Die abstrakte Deklaration des Konstruktors des Objekts unterscheidet sich zunächst nicht von der Deklaration einer Funktion. Der Unterschied liegt in der Benutzung von new
. Folgender Ablauf passiert intern:
- Erzeuge zuerst ein neues leeres Objekt
{ }
- Setze dann die
constructor
-Eigenschaft auf die Konstruktorfunktion - Setze die Basisklasse
Object
auf den Prototype der FunktionmyObject.prototoype = { }
- Rufe die Konstruktorfunktion im Kontext des neuen Objekts auf, das heißt mit
this
als Verweis auf das Objekt.
Wird nun das Schlüsselwort this
zusammen mit einer Eigenschaft oder einer Methode verwendet, wird auf das neue Objekt verwiesen, nicht auf die Deklaration. Praktisch hat man so eine Unterscheidung zwischen der Deklaration (lies: Klasse) und der Instanz.
Nachdem der Prototyp des Objekts angepasst wurde, kann wieder eine Instanz unter Verwendung des Schlüsselwortes new
erstellt werden. Die Instanz wird verwendet, indem die Eigenschaft oder Methode mit einem Punkt “.” getrennt aufgerufen wird. Alternativ ist auch hier die Schreibweise mit der Index-Notation möglich: cat["talk"]()
.
Besonders hervorzuheben ist die Funktion, das bereits existierende Objekt von den per Prototyp hinzugefügten Funktionen und Eigenschaften profitieren. Dies ist aber nur dann der Fall, wenn bei der Deklaration keine Definition erfolgte. Im folgenden Beispiel “lernen” die Instanzen die Funktion talk erst später:
Anders sieht es aus, wenn es die Funktion bereits gibt:
Der Prototyp wird zwar geändert, aber die Instanz-Funktion ist davor erstellt und deshalb im Suchbaum zuerst dran. Der Interpreter findet diese Stelle erst und führt sie aus. Der Prototyp wird nicht mehr erreicht – er wurde überdeckt1. Das ist das normale Verhalten, denn der Prototyp dient lediglich dazu, eine Standarddefinition bereitzustellen, für den Fall, dass es keine explizite Definition gibt. Gibt es diese aber, ist es nicht mehr notwendig, im Prototyp nachzuschauen. Die folgende Grafik zeigt den Suchbaum.
Die Ausgabe von ‘stumm’ ist im finalen Objekt und der Suchbaum kommt nicht mehr zur Anwendung.
Methoden und Eigenschaften
Das Hinzufügen von Methoden und Eigenschaften zu Laufzeit ist der primäre Zweck der Eigenschaft prototype
. Sie bekommen nachträglich Zugriff auf die Definition des Objekts. Auf diese Weise kann das vorletzte Beispiel schnell um eine Funktion zum Umbenennen erweitert werden.
Der Eigenschaft prototype
des Objekts Cat wird eine Funktion changeName (Zeile 1) hinzugefügt, indem diese einfach zugewiesen wird. Auf diese Weise wäre es denkbar, einem Browser, welcher Objekte enthält, denen Funktionen oder Eigenschaften fehlen, diese quasi nachzurüsten.
Umgang mit Eigenschaften
ECMAScript erlaubt die Deklaration von getrennten Get- und Set-Methoden für Eigenschaften. Das folgende Listing 2.2 zeigt eine solche Deklaration:
Erfolgt nun der Aufruf der Eigenschaft area, so wird 1000 ausgegeben. Tatsächlich handelt es sich hier – wie bei vielen anderen Programmiersprachen auch – um Eigenschaftsmethoden. Die Funktionsklammern ()
werden weggelassen, wenn die Schlüsselwörter set
beziehungsweise get
benutzt worden sind.
Private Variablen und Funktionen
In JavaScript sind alle Mitglieder öffentlich. Jede Funktion kann auf diese Mitglieder zugreifen, weitere hinzufügen, diese verändern oder entfernen. Jedoch gibt es die Möglichkeit, private Mitglieder zu erzeugen, auf die nur von privilegierten Funktionen zugegriffen werden kann. Dazu wird die Eigenschaft von JavaScript benutzt, dass Variablen in Funktionen nur in diesem Gültigkeitsbereich sichtbar sind.
Private Mitglieder werden innerhalb des Konstruktors deklariert. Eigenschaften werden mit Hilfe des Schlüsselwortes var
angelegt. Funktionen werden innerhalb des Konstruktors definiert.
Der Konstruktor für das Objekt SomeObject wird definiert (Zeile 1). Innerhalb des Konstruktors wird eine Variable für die Instanzen des Objekts festgelegt, member. Der Parameter param wird der öffentlichen Variablen member (Zeile 11) zugewiesen. Die Methode dec ist privat.
Die private Methode dec (Zeile 3) kann auf die privaten Eigenschaften zugreifen, weil diese innerhalb der gleichen Funktion (dem Konstruktor) deklariert sind. Öffentliche (public) Funktionen können nicht direkt auf private Funktionen und Variablen zugreifen. Hierfür sind sogenannte privilegierte Funktionen erforderlich. So führt folgender Aufruf zu einem Fehler:
Privilegierte Funktionen
Eine privilegierte Funktion kann auf private Eigenschaften und Funktionen zugreifen, ist jedoch selbst öffentlich zugreifbar. Alle Funktionen, welche innerhalb des Konstruktors mit Hilfe des this
-Operators zugewiesen werden, sind privilegierte Funktionen.
Die privilegierte Funktion (Zeile 16) kann sowohl auf die privaten Funktionen (Zeile 12) und Eigenschaften als auch auf öffentliche Mitglieder (Zeile 17) zugreifen. Sie selbst ist als Teil des Prototypen öffentlich. Die Ausgabe dieses Beispiels zeigt internes deterministisches Verhalten. Es wird dreimal der Wert ausgegeben, der im Konstruktor übergeben wurde, danach null
.
2.2 Vererbung
Vererbung in JavaScript basiert generell auf Prototypen, auch wenn das neue Schlüsselwort extends
in Klassen benutzt wird.
Prototypische Vererbung
Prototyping ist in JavaScript die Technik, die beim Umgang mit Objekten die fehlenden Vererbungsfunktionen ersetzt. Zuvor jedoch eine kurze Wiederholung der Eigenschaften von Objekten.
Objekte sind Dictionaries (Maps) und können sehr flexibel erstellt werden. Die interne Umsetzung erlaubt alternativ zur intuitiven Objektschreibweise auch die Nutzung einer Art Array-Syntax. Hier ein einfaches Objekt:
Alternativ kann die Zuweisung der Eigenschaften auch sofort erfolgen:
Das letzte Beispiel definiert second() als Methode. Alternativ zum Punkt kann auch folgende Schreibweise benutzt werden:
Der Vorteil der Array-Schreibweise liegt in der Möglichkeit, das Argument durch eine Variable zu ersetzen und Eigenschaften und Methoden so dynamisch nutzen und erzeugen zu können.
Objektabstammung
Alle Objekte stammen von Object
ab. Betrachten Sie ein weiteres Objekt und eine Methode:
Dies funktioniert, obwohl hier vorab die Methode valueOf()
nicht definiert wurde. Das liegt daran, dass jedes Objekt von der internen Klasse Object
erbt und diese bringt die Methode valueOf()
mit – valueOf()
ist im Prototyp der Klasse Object
definiert.
Prototypisch geerbte Methoden können überdeckt werden:
Das Überdecken führt dazu, dass nun die eigene Definition ausgeführt wird.
Die Basismethoden
Folgende eingebaute Methoden stehen für alle Objekte zur Verfügung:
-
create
: Eine weitere Instanz aus dem Objekt ableiten (clonen) -
defineProperties
: Einige Eigenschaften erstellen -
defineProperty
: Eine Eigenschaft erstellen -
freeze
: Ein Objekt “einfrieren”, um künftige Änderungen zu verhindern -
isFrozen
: Gibt an, ob eine Objekt dynamisch erweitert werden kann (true
zeigt an, dass dies nicht der Fall ist) -
hasOwnProperty
: Feststellen, ob eine Eigenschaft geerbt oder im Objekt deklariert wurde -
toString
: Zeichenkettenform ausgeben
Folgende eingebaute Eigenschaft existiert:
-
prototype
: Zugriff auf den Prototypen – die Standarddefinition
Objektvererbung
Schauen Sie zuerst auf eine Objektdefinition. Von dieser wird sodann eine Instanz angelegt, die unmittelbar erbt:
Das Anlegen von Instanzen erfolgt mit Object.create
. Der Prototyp des Objekts, der die zu erbenden Elemente enthält, wird dabei quasi nebenbei erzeugt. Dies erlaubt freilich nicht die Nutzung einer Konstruktor-Funktion, die das Objekt mit Stammeigenschaften versehen könnte. Das Objekt point3d enthält deshalb nicht, wie vielleicht erwartet, die Eigenschaften x und y. Um dies zu erreichen, kann folgendes Pattern helfen:
Mit einer Konstruktorfunktion sieht das dagegen folgendermaßen aus:
Hier wird als “Erbmasse” der Inhalt der Eigenschaft prototype
der Funktion SomeConstructor
benutzt. Die ist vorhanden, weil function
selbst ein Objekt ist und dieses Verhalten von Object
geerbt hat. Das Schlüsselwort new
greift also auf SomeConstructor.prototype
zu.
2.3 Exkurs Objekthierarchie
In diesem Abschnitt sollen einige Aspekte der objektorientierten Programmierung anhand eingebauter Funktionen vertieft werden.
Umgang mit this
Die Funktion call
, die jedes Funktionsobjekt kennt, ruft die Funktion auf und übergibt ein Objekt als this
in den Sichtbereich dieser Funktion. Optional werden Argumente übergeben.
function.call(obj, arg, arg, arg)
Die Funktion apply
funktioniert wie call
, aber die Argumente args sind ein Objekt vom Typ Array.
function.apply(obj, args)
Im Beispiel manipuliert der Aufruf in Zeile 18 das this
in Zeile 9. Der Aufruf in Zeile 16 gibt NaN
(not a number) zurück, weil this
bei der Benutzung von Referenzen – dies passiert durch die Zuweisung in Zeile 15, auf undefined
gesetzt wird.
Des weiteren kommt die Methode bind
zum Einsatz, die das Referenzobjekt this
einmalig fest bindet und künftige Manipulationen unterdrückt.
Die Ausgabe ist: “OK”, “undefined”, “OK”. Die Ausgabe “undefined” kommt zustande, weil beim Aufruf von looseClick keine Referenz auf this
vorhanden ist. Bei boundClick dagegen wurde diese zuvor mit bind
festgelegt. Der nächste Abschnitt “Vertiefung zu Bindungen” geht weiter darauf ein.
Bindungen
Als Bindung ist in Programmiersprachen der Zeitpunkt der Bereitstellung von Variablen in Abhängigkeit vom Programmfluss gemeint. Normalerweise müssen Sie sich als Entwickler selten um die Bindung kümmern. Dies erledigt der Compiler oder Interpreter. Die Reihenfolge der Definition spielt keine Rolle. Darüber hinaus ist es üblich, dass Zugriffe ohne explizite Angabe des Sichtbereiches immer das aktuelle Objekt adressieren. Die explizite Angabe einer Zugriffsform mit einem Schlüsselwort wie this
ist meist möglich, aber nicht notwendig. Wenn Sie von einer anderen Programmiersprache auf JavaScript wechseln, sollten Sie unbedingt beachten, das JavaScript (neben PHP übrigens), die große Ausnahme von dieser Regel ist. JavaScript erfordert eine explizite Angabe des Sichtbereiches, auch wenn es sich lediglich um die aktuelle Methode handelt.
Das liegt im Wesentlichen daran, dass JavaScript im traditionellen Sinne keine rein objektorientierte Sprache ist. Die prototypische Vererbung, die zuvor bereits vorgestellt wurde, ist eine andere Art des Umgangs mit Objekten. Damit einher geht ein anderes Bindungsverhalten – die Bindung ist immer explizit. Die explizite Bindung bezieht mit ein, dass das aktuelle Objekt nicht das ist, was man als Entwickler intuitiv erwartet. Dies wurde bereits bei der Vorstellung der Methode call
gezeigt.
Das folgende Beispiel zeigt, was passiert, wenn dieses Verhalten nicht beachtet wird.
Da ist sicher nicht das erwartete Verhalten. Der Name erscheint nicht wie gedacht. Das Problem ist der Zugriff auf die Objekteigenschaft name in Zeile 4. Hier fehlt die Bindungsreferenz. Die Annahme, es würde schon das aktuelle Objekt sein (Joerg aus Zeile 1), ist schlicht falsch. Hier geht JavaScript einfach auf Suche nach dem Namen oberhalb der Definition. Das Ende ist das Objekt window
, das keine Eigenschaft name hat und deshalb ist der Name leer (der Fehler wirkt sich nicht drastischer aus, weil der Rückgabewert undefined
hier in eine leere Zeichenkette konvertiert wird).
Nun ein weiterer Versuch:
Das funktioniert nur scheinbar richtig. Denn nun wurde tatsächlich dynamisch eine neue Eigenschaft name dem window
-Objekt hinzugefügt. Globale Namen auf dieser Ebene sind aber ein Antipattern – vermeiden Sie das, wann immer es geht.
Korrekt ist die Benutzung von this
:
Das Schlüsselwort this
ist hier die explizite Bindung an einen bestimmten Kontext – nämlich den des Aufrufers – was in diesem Fall das aktuelle Objekt ist.
Ein weiteres Beispiel zeigt, wie schnell auch this
tückisch werden kann. Typisch sind Referenzen auf Funktionen, wie hier in Zeile 7:
Trotz der Angabe der expliziten Bindung funktioniert das nicht wie erwartet. this
hat hier offensichtlich eine andere Bedeutung bekommen. Bei Referenzen geht die Bindung verloren, this
weiß nicht, wo es herkommt. Hier hilft nur, die Bindung beim Aufruf explizit zu steuern. Ansonsten sucht JavaScript wieder den Objektbaum aufwärts und endet erneut bei window
(im Browser) oder global
(in Node).
Bindungen berücksichtigen
Code sollte die Bindungen explizit berücksichtigen. Wenn Sie eine Methode als Argument einer anderen Methode übergeben, dann gelangt die ausführende Instanz in den Kontext des Aufrufers und kann dann lokal binden. Hier ein Beispiel, zuerst mit einer Klassendefinition:
Nun wird dieser Code benutzt:
Das funktioniert soweit gut. Nun erfolgt eine Erweiterung:
Hier ging die Bindung wieder verloren – JavaScript liest undefined
. Nun kann das Problem leicht gelöst werden. Wenn der Aufruf jedoch in ein Framework geht und die Bindung nicht sichtbar ist, kann es sehr schwer sein, die Ursache für die Fehlfunktion zu finden. Hier ein Beispiel:
Dies funktioniert nicht, weil markItemAsProcessed über this
nicht gefunden wird. Die Methode each stammt aus einer Bibliothek, auf die per Referenz verwiesen wird. Diese Referenz “bricht” den Zugriff (die Bindung) auf das aktuelle Objekt, indem die Methode markItemAsProcessed definiert wurde. Der Aufrufer bestimmt hier, was this
tatsächlich ist. Das kann funktionieren, muss aber nicht. Meist wird this
in der Tat manipuliert, um einen bequemen Zugriff auf einen Kontext oder eine Ereignisquelle zu erhalten.
Explizite Bindungen
Das explizite Binden erfolgt über die Methoden apply
, call
und bind
. Jedes Funktionsobjekt (lies: Funktion), verfügt über diese Methode – sie werden vom Funktionsprototypen geliefert.
Der Aufruf (Zeile 2) erfolgt hier über apply
. Der Kontext für die Bindung ist das erste Argument. Danach folgt ein Array mit allen anderen Argumenten der indirekt aufgerufenen Methode – egal wie viele das sind.
Die Methode call
unterscheidet sich nur insofern, als dass hier die Argumente explizit angegeben werden und passen müssen.
Besonders tückisch ist die Tatsache, dass nach dem Verlust der Bindung die explizite Bindung jedes Objekt akzeptiert. Es muss überhaupt kein Zusammenhang mit dem aktuellen Code bestehen. Das kann zu komplett unwartbarem und vor allem auch unlesbarem Code führen. Disziplin ist hier unerlässlich.
Solche freien Bindungen sind selten sinnvoll. Besser wäre es, man könnte die Bindung beschränken. Das geht nur über das Verschachteln von Funktionen, bei denen das durchgereichte Kontextobjekt unter Kontrolle ist:
Das wird benutzt wie nachfolgend gezeigt:
Das funktioniert, die Referenz ist festgelegt. Sie kann von außen auch nicht gestört werden.
2.4 Klassen
Klassen in ES2015 sind lediglich syntaktischer Zucker über dem Prototyp-Pattern, das bereits beschrieben wurde. Wenn Prototypen nicht verstanden wurden, helfen Klassen nicht wirklich weiter, weil sie sich nicht wie in anderen Sprachen verhalten (das gilt übrigens auch für TypeScript).
Unterstützt werden neben Prototypen auch Aufrufe der Basisklasse mittels super
, die Bildung von Instanzen mit new
und statische Methoden sowie Konstruktoren.
Mehr Details zu dieser Syntax folgt in diesem Kapitel, nachdem einige Grundlagen gelegt werden, die zum Verständnis sehr sinnvoll sind.
Der Konstruktor constructor
Zur Vereinfachung der Syntax steht das Schlüsselwort constructor
zur Verfügung:
Es kann Argumente haben. Es muss in einer Klasse class
benutzt werden.
Statische Mitglieder
Klassen können statische Mitglieder enthalten. Diese können aufgerufen werden, ohne das erst eine Instanz der Klasse erstellt wird. Erreicht wird dies mit dem Schlüsselwort static
(Listing 2.6).
Der Zugriff kann auch im Konstruktor erfolgen.
Vererbung mit extends
Wird das Objektkonstrukt mit class
erstellt, so kann von einer anderen Klasse mit extends
geerbt werden.
Der Konstruktor der geerbten Klasse muss explizit aufgerufen werden. Dazu dient das Schlüsselwort super
(Zeile 9).
Ein anderes Beispiel (Listing 2.9) zeigt, wie der Prototyp überschrieben werden kann.
Dies ist quasi eine Mischung aus der protypischen Vererbung und dem syntaktischen Zucker mit class
.
2.5 Internes
Intern sind noch ein paar Techniken im Einsatz, deren Kenntnis hilfreich zum Verständnis sein kann. In der Praxis wird man damit nicht zwingend arbeiten, vor allem weil es hier Unterschiede zwischen verschiedenen JavaScript-Laufzeitumgebungen gibt.
Umgang __proto__
Die Eigenschaft prototype
ist nur für Funktionen definiert:
Function.prototype
Dies ist eine Funktion zum Anlegen von Prototyp-Mitgliedern, also vererbbaren Elementen. __proto__
dagegen ist der interne via prototype
geerbte Bestand an Mitgliedern. __proto__
ist daher immer verfügbar, egal ob Funktion oder nicht.
Die Objektvererbung mittels __proto__-Prototype
Schauen Sie sich folgendes Listing 2.10 an:
Das interne Mitglied __proto__
liefert die final ererbten Elemente. Wird es überschrieben, ergibt der zugewiesene Inhalt den finalen Bestand an Mitgliedern.
2.6 Ableiten interner Typen
Einige interne Klassen, wie Date
und Array
, können als Basisklassen für eigene Konstrukte benutzt werden.
Der Aufruf von super
hängt vom Basistyp ab. Der Spread-Operator ist ein bequemes Mittel, flexibel mit Argumentlisten zu arbeiten.
2.7 Tipps
Ein paar allgemeine Regeln zum Umgang mit objektorientierten Techniken sollen dazu dienen, JavaScript hier besser einordnen zu können:
- Keine tiefen Hierarchien
- Die Dynamik ist die Stärke, nicht tiefe Klassenmodelle
- Vererbung ist eine Option, es gibt andere Pattern
- Klassen vor allem zu Steuerung des Scope
- Private versus Öffentlich ist essenziell
- Weniger ist mehr
3 Globale Standardfunktionen
Zu den Standardfunktionen gehören einige global verfügbare Methoden, die universell benutzt werden können. Sie sind im Browser in window
definiert, in Node dagegen in global
, sodass man davon ausgehen kann, dass sie immer verfügbar sind.
3.1 Zeitgeberfunktionen
Folgende Methoden stehen zur Verfügung:
setTimeout
clearTimeout
setInterval
clearInterval
Einsatzfälle
Die Funktionen führen eine andere Funktion nach einer bestimmten Zeitspanne aus, beim Intervall wiederholt sich dies bis zum Löschen regelmäßig.
Bei der Timer-Funktion kann der Wert 0 sein, sodass die aufgerufene Funktion sofort startet. Der Trick ist hier, dass dies quasi asynchron, in einem neuen Thread passiert. JavaScript hat selbst keine Thread-Verwaltung und es müssen immer Hilfsfunktionen benutzt werden, um Aktionen außerhalb des Haupt-Threads auszuführen. Notwendig und sinnvoll ist das, damit die Oberfläche bei Aktionen die länger als 50ms sind, nicht blockiert. 50ms ist eine Dauer, so ein Benutzer bereits Verzögerungen spürt (als ruckeln beispielsweise).
Viele Bibliotheken, die asynchrone Aufrufe anbieten, nutzen setTimeout
intern.
Anwendung
Nutzen Sie immer Funktionen für den Aufruf, nie Zeichenketten. Zeichenketten sind langsam und unsicher. Folgendes geht, ist aber nicht so gut:
Das Problem ist hier vor allem, dass im Laufe der Zeit sich Änderungen am Code ergeben, die in Zeichenketten jedoch nicht von Werkzeugen ausgewertet werden können. So entstehen Fehlerquellen. Minifier, die JavaScript verdichten, werden Zeichenketten nie anfassen (es könnte Text sein, der ausgegeben wird). Damit sind lange Funktionsnamen von der Optimierung ausgeschlossen.
Folgendes ist besser:
Hier ist zu beachten, dass der Funktionsname ohne Klammern geschrieben wird. Es handelt sich dabei um einen Funktionszeiger (pointer), nicht um den eigentliche Aufruf (um den kümmert sich die JavaScript-Engine später).
Noch einfacher ist es, die Funktion gleich anonym zu benutzen:
3.2 Funktionen für Zahlen
Funktionen für numerische Werte helfen die Mängel bei den Typen zu kompensieren.
Prüf-Funktionen
Zwei Tests für Zahlen können direkt ausgeführt werden:
* isFinite()
: Test auf gültigen Zahlenbereich
* isNaN()
: Test auf NaN – Not a Number
Parser-Funktionen
Parser-Funktionen für Zahlen sind folgende:
* parseFloat()
: Erkennt Gleitkommazahlen aus einer Zeichenkette
* parseInt()
: Erkennt Integer aus einer Zeichenkette
3.3 Konverter-Funktionen
Hier ist der Umgang mit einem URI wichtig, weil dort spezielle Zeichen nicht erlaubt sind und kodiert werden müssen.
decodeURI()
decodeURIComponent()
encodeURI()
encodeURIComponent()
Die Ausgabe berücksichtigt, dass Leerzeichen nicht erlaubt sind und durch %20 ersetzt werden:
https://somewebsite.de?p=First%20Param&q=Last%20Param
Die Schreibweise %HH ist der Hexcode in der erweiterten ASCII-Tabelle. Für Zeichen nach UTF-8 wird gegebenenfalls %HHHH benutzt.
4 Module
JavaScript ist eine Sprache, die nicht durch besonders viele Schlüsselwörter glänzt. Die Einfachheit hat seinen Preis, wenn umfangreichere Applikationen entstehen. Damit trotz der Beschränkungen eine hohe Code-Qualität möglich wird, sind viele Entwurfsmuster (pattern) entstanden, die die fehlenden sprachlichen Ausdrucksmöglichkeiten ersetzen.
4.1 Modul-Entwurfsmuster
Das Modul-Entwurfsmuster ist eines der am häufigsten eingesetzten. Mangels Namensräumen ist die Isolation von Code von herausragender Bedeutung. Mit dem Modul-Entwurfsmuster wird genau das erreicht. Darüber hinaus kann hier auch eine Aufteilung des Modulcodes auf verschiedene Dateien erfolgen. Dies erleichtert die Übersichtlichkeit und Wartbarkeit durch Modularisierung – daher der Name.
Zum Vergleich soll hier ein Blick auf ein klassisches unstrukturiertes Skript erfolgen. Zahlreiche Skripte, die Sie im Netz finden können, liegen in einer gesonderten Datei vor und sind darüber hinaus unstrukturiert. Es handelt sich um eine lose Sammlung von globalen Variablen und Funktionen:
Diese Organisation führt dazu, dass das Skript nicht gut konfigurierbar, anpassbar und erweiterbar ist. Am schwersten wiegt jedoch, dass es sich um eine große Zahl von Objekten im globalen Sichtbarkeitbereich handelt. Globale Variablen und Funktionen sind Eigenschaften des window
-Objektes, wenn JavaScript im Browser läuft, bzw. global
in Node. Ein schwaches Hilfsmittel besteht oft darin, benutzerspezifische Präfixe zu benutzen:
Unstrukturierte Skripte sind schlecht zu warten und kollidieren mit anderen Skripten. Die Namensgebung ist in der Regel ein schwacher und unsicherer Schutz. Es ist naheliegend, das globale Variablen – sorgfältig benannt oder nicht – vermieden werden sollten. Insbesondere ist dies deshalb beachtenswert, weil der globale Adressraum im Browser auch das Objektmodell der HTML-Seite enthält und Konflikte sich unmittelbar schädlich auf das Browserverhalten und die Anzeige auswirken können.
Der Kapselung der eigenen Codes kommt also große Bedeutung zu. Darum geht es im Modul-Entwurfsmuster. Datenkapselung bedeutet, dass das Erweitern des globalen Objekts sowie der DOM-Objekte auf ein Minimum reduziert wird. Sie sollten nur dann Objekte im window
-Objekt speichern, wenn der Zugriff von außen unbedingt notwendig sind.
Die öffentliche API Ihres Skriptes benötigt nur ein globales Objekt, über welches die restlichen Funktionen zugänglich sind. Bei manchen Aufgaben ist es möglich, ein Skript konsequent zu kapseln, sodass es das globale window
-Objekt überhaupt nicht antastet. In anderen Fällen ist es nötig, zumindest ein Objekt global verfügbar zu machen. Große Bibliotheken dienen hier oft als Vorbild. Das große jQuery-Framework definiert standardmäßig nur zwei globale Variablen: window.jQuery()
und als Alias window.$()
. Das YUI-Framework definiert lediglich window.YUI()
.
Einfache Module
Ausgangspunkt für ein Modul ist ein Objekt, dass meist durch das Objekt-Literal erstellt wird. Alle Variablen und Funktionen eines Skripts werden in einer solchen Objektstruktur untergebracht. Im globalen Geltungsbereich taucht dann nur noch diese eine Objektstruktur auf, andere globale Variablen oder Funktionen werden nicht belegt. Das Skript ist in der Objektstruktur in sich abgeschlossen. Damit sind Wechselwirkungen mit anderen Skripten ausgeschlossen, solange der Bezeichner der Objektstruktur eindeutig ist.
Ein JavaScript-Objekt ist also nichts anderes als ein Container für weitere Daten. Ein Objekt ist eine Liste, in der unter einem Bezeichner Unterobjekte gespeichert sind.
Hier ist nun nur noch das Objekt jk_obj im globalen Gültigkeitsbereich. Das ist schon sehr gut, allerdings zeigt der Code auf Zeile 14, das es keinen Schutz der intern benutzten Eigenschaften gibt. Eine ungewollte Manipulation kann fatale Folgen haben.
Privater Funktions-Scope
Eine Kapselung erreichen Sie mit einer Funktion, die Ihre Variablen einschließt und nur wenige Objekte nach außen verfügbar macht.
Beim Objekt-Literal wird ein globales Objekt als Namensraum benutzt, um darin eigene Objekte unterzubringen. Diese Objekte sind über das Containerobjekt für andere Skripte zugänglich. Es gibt also keine Trennung zwischen öffentlichen und privaten Daten. Während es sinnvoll ist, dass beispielsweise eine Methode Modul.methode() von außen aufrufbar ist, ist es unnötig und potenziell problematisch, dass jede Objekteigenschaft gelesen und manipuliert werden kann.
Der nächste Schritt ist daher, eine wirksame Kapselung zu implementieren. Das Mittel dazu ist ein eigener, privater Variablen-Gültigkeitsbereich. Darin können beliebig viele lokale Variablen und Methoden definiert werden. Die einzige Möglichkeit, in JavaScript einen solchen Gültigkeitsbereich zu erzeugen, ist eine Funktion. Sie definieren also zuerst eine Funktion, um darin das gesamte Skript zu kapseln. Solange durchgehend lokale Variablen und Funktionen verwendet werden, wird der globale Gültigkeitsbereich nicht angetastet.
Schließen Sie Ihren Code in einen Funktionsausdruck ein, der sofort ausgeführt wird. Folgendes Fragment zeigt, wie dies aussieht:
Hier wird eine namenlose Funktion per Funktionsausdruck erzeugt (Zeile 1). Diese wird mit Klammern umschlossen. Auf diesen Ausdruck wird der Call-Operator ausgeführt (Zeile 3) – die runden Klammern am Ende ()
. Die Anweisung wird mit einem Semikolon abgeschlossen.
Diese anonyme Funktion wird nur notiert, um einen Gültigkeitsbereich zu erzeugen, und sie wird sofort ausgeführt, ohne dass sie irgendwo gespeichert wird. Innerhalb der Funktion wird nun der gewünschte Code untergebracht:
Im Beispiel finden sich Variablendeklarationen und eine Funktionsdeklarationen. Beide sind lokal, sind also nur innerhalb der Kapselfunktion zugänglich. Sie können auf die Variablen und Funktionen intern direkt zugreifen.
Vergessen Sie nicht, Variablen mit var
oder let
als lokal zu deklarieren. Andernfalls werden sie automatisch global, also Eigenschaften von globalThis
.
Öffentliche Schnittstellen
Auch das letzte Muster ist nicht perfekt. Denn die Funktion wird genau einmal ausgeführt und dann besteht keine Zugriffsmöglichkeit mehr. Das kann ausreichend sein – meist reicht es jedoch nicht.
Das Schnittstellen-Entwurfsmuster (Revealing Module Pattern) erlaubt öffentliche und private Objekte und eignet sich ideal, um API und interne Implementierung sauber zu trennen. Diesen Kompromiss erreichen Sie durch eine Kombination aus Objekt-Literalen und einer Kapselfunktion.
Die grundlegende Struktur sieht folgendermaßen aus:
Auf das vorherige Beispiel angewendet sieht das nun folgendermaßen aus:
In diesem Beispiel wird genau ein globales Objekt belegt, sodass der Zugriff wiederholt erfolgen kann. Darüber hinaus wird eine Art Schnittstelle – ein Interface – definiert, dass die Zugriffsform regelt. Dies wird durch die return
-Anweisung in Zeile 8 erreicht. Hier wird ein weiteres Objekt mittels Objekt-Literal erstellt, in dem zwei Methoden erstellt werden, die ihrerseits auf private Mitglieder des Moduls zugreifen. Nach außen sind jetzt nur die Methoden show und increment sichtbar.
Instanzen von Modul-Objekten
Nun kann es passieren, dass ein Modul nicht nur eine statische Umgebung liefert, sondern mehrere Instanzen davon abstammen, die unabhängig voneinander sind. Die im Abschnitt Objektorientierung gezeigte Form der Nutzung von Funktionen als Konstruktor wird nun mit dem Modul-Entwurfsmuster kombiniert.
Erneut wird nur ein einziges Objekt im globalen Namensraum benötigt. Die Variable counterInitialValue ist intern und geschützt (Zeile 2). Auch der Konstruktor (Zeile 3) ist intern. Damit Instanzen die Methoden erben, werden diese auf dem Prototypen des Konstruktors definiert (Zeilen 6 und 9). Die Konstruktorfunktion liefert also alles, was benötigt wird. Und nur diese ist auch öffentlich (Zeile 13).
Die Benutzung dieses Moduls – wo auch immer – sieht nun folgendermaßen aus:
Globale Objekte
Das Übergeben von Objekten in die Kapselfunktion verkürzt die Gültigkeitskette und beschleunigt den Zugriff auf diese Objekte. Ohne diese Übergabe wäre JavaScript gezwungen, immer alle erreichbaren Gültigkeitsbereich abzusuchen, ob das gewünschte Objekt nicht irgendwo erreicht werden kann. Ein Wert, der als Parameter übergeben wurde, ist in der Suchliste weit oben und wird sehr schnell gefunden.
Aus diesem Grund hat es sich eingebürgert, das window
-Objekt sowie weitere häufig benutzte Objekte wie document
mittels Parametern in den Funktions-Gültigkeitsbereich zu übergeben:
Gleichzeitig wird hier sichergestellt, dass innerhalb der Funktion der Bezeichner undefined
immer den Typ undefined
besitzt. Wir definieren einen solchen Parameter, aber übergeben keinen Wert dafür –- sodass eine lokale Variable namens undefined mit einem leeren Wert angelegt wird. Das ist andernfalls nicht garantiert, denn window.undefined
ist durch Skripte überschreibbar.
Erweiterbare Module
Eigene Module werden genauso übergeben und können damit leicht erreicht werden. Zugleich erlaubt diese Technik das Aufteilen des Skripts auf mehrere Dateien. Module nachträglich zu erweitern ist ebenso sinnvoll und häufig benötigt, allerdings haben die einzelnen Teile keinen Zugriff auf die privaten Objekte der anderen Teilmodule.
Ein selbst definiertes Modul könnte nun folgendermaßen aussehen:
In einem weiteren Skript kann dieses Modul erweitert werden:
Problematisch ist hier, dass die Reihenfolge der Definition entscheidend ist. Beim Laden der Skripte via HTTP kann die Reihenfolge aber nicht garantiert werden. Deshalb soll der default-Operator ||
benutzt werden, um einen beliebigen Anfangspunkt zu erlauben:
Dieses Verfahren wird auch als “lose Augmentierung” bezeichnet. Die Funktionsweise ist einfach. Jedes Modul wird gleichartig definiert. Das zuerst ausgeführte Skript – egal welches – findet die Variable MODULE im Zustand undefined
vor. Damit ist der erste Teil des Ausdrucks in Zeile 6 false
. Dadurch wird der zweite Teil ausgeführt – ein leeres Objekt. Dies wird in der Kette der Modulbausteine als Parameter übergeben und dann mit Eigenschaften und Methoden sukzessive angereichert (daher der Name “Augmentierung”).
Die lose Augmentierung ist nicht immer sinnvoll, denn sie schränkt die Möglichkeiten objektorientierter Techniken etwas ein. Eine feste Augmentierung – mit definierter Reihenfolge – kann beispielsweise benutzt werden, um bereits definierte Funktionen unter bestimmten Umständen gezielt zu überschreiben oder zu verdecken.
Namensräume
Module können Sie mit einem Objekt in einem Namensraum gruppieren. Es gibt zwar keine nativen Namensräume in JavaScript, aber gekapselte, verschachtelte Objekte erfüllen denselben Zweck – ganz ohne ein weiteres Schlüsselwort.
Zusammenfassung
Entwurfsmuster sind essenziell in JavaScript. Die Programmierung erfordert Disziplin, ohne die schnell unwartbarer Code entsteht. Auf dem Weg zu gutem Code helfen Entwurfsmuster. Nutzen Sie diese auch bei kleinen und trivialen Projekten, um schnell Sicherheit zu gewinnen.
Die Techniken dienen vor allem dazu, die fehlenden sprachlichen Merkmale von JavaScript auszugleichen. Sie zeigen auch, das komplexe Sprachen mit vielen Schlüsselwörtern nicht zwingend erforderlich sind, um große Projekt umzusetzen.
4.2 Native Module
Native Module werden seit ES2015 direkt auf Sprachebene unterstützt. Damit sind Standard-Loader wie AMD und CommonJS direkt einsetzbar. Das Laden der Module erfolgt asynchron und das Verarbeiten implizit synchron, sodass das Laden effektiv und die Verarbeitung intuitiv ist.
Das Exportieren (Bereitstellen) eines Moduls erfolgt mit export
:
Beim Import erfolgt die Angabe in geschweiften Klammern für die exportierten Mitglieder. Wird ein Name direkt – ohne Klammern – benutzt, so wie im folgenden Beispiel der Name exp, so wird das mit default
gekennzeichnete Mitglied geladen. Es darf immer nur ein Mitglied einer Datei so bezeichnet werden.
5 Funktionen
Dieses Kapitel behandelt die vielfältigen Einsatzmöglichkeiten von Funktionen und anonymen Funktionsaufrufen.
5.1 Funktionsargumente
Bei der Arbeit mit Funktionsargumenten gibt es einige Besonderheiten im Vergleich zu anderen Sprachen.
Standardargumente
Argumente lassen sich mit Standardwerten belegen, die angewendet werden, wenn der Aufrufer keine Werte liefert.
Argument-Arrays
Eine beliebige Anzahl Argumente lässt sich mit dem ...
-Operator an ein Array binden.
Das lässt sich auch umdrehen und beim Aufruf benutzen:
Rest-Parameter
Der Parameter-Operator ...
(auch Rest-Operator) stellt die Parameter als Array bereit.
Es gibt Unterschiede zum bereits gezeigten arguments
-Objekt:
- Nur namenlose Argumente werden erfasst, bei
arguments
sind es immer alle -
arguments
ist kein echtes Array, sondern ein spezielles Objekt; der Rest-Operator erzeugt aber ein echtes Array -
arguments
verfügt über spezielle Funktionen, die ein Standard-Array nicht hat
5.2 Lambda-Funktionen
Lambda-Funktionen sind anonyme Funktionsaufrufe, die den Operator =>
1 nutzen, um den Schreibaufwand zu reduzieren. Das ist ähnlich zur Syntax in C# oder Java 8. Der Zeiger this
hat denselben Scope wie der umliegende Code, nicht einen eigenen, funktionsspezifischen wie bei der Benutzung von function
. Auch das spezielle Argument-Array arguments
wird aus der umgebenden Funktion übernommen und es wird kein Neues gebildet.
Hier ein paar Beispiele:
Eine Lambda-Funktion bindet kein eigenes this
, arguments
, super
, oder new.target
. Lambda-Funktionen sind immer anonym.
Funktionsrumpf
Lambda-Funktionen können entweder einen kurzen oder einen Block-Rumpf haben.
In einem kurzen Rumpf ist lediglich ein Ausdruck nötig und eine implizite Rückgabe wird angehängt. In einem Block-Rumpf muss eine explizite Rückgabeanweisung verwendet werden.
Rückgabe von Objekt-Literalen
Man bedenke, dass die Rückgabe von Objekt-Literalen unter Verwendung der kurzen Syntax nicht so ausgeführt wird, wie man es erwarten würde:
Die Aufrufreihenfolge ist stattdessen:
Der Aufruf von func() (Zeile 1) gibt undefined
zurück! Der zweite Ausdruck erzeugt einen Syntaxfehler, die function
-Anweisung erfordert einen Namen. Der Grund dafür ist, dass der Code in geschweiften Klammern ({}) als eine Sequenz von Anweisungen übersetzt wird (d.h. foo wird als Bezeichner behandelt und nicht als Schlüssel eines Objekt-Literals).
Man bedenke, das Objekt-Literal in Klammern zu setzen:
Zeilenumbruch
Lambda-Funktionen können keinen Zeilenumbruch zwischen Parametern und dem Pfeil haben.
Übersetzungsreihenfolge
Der Pfeil innerhalb einer Pfeilfunktion ist kein Operator. Allerdings haben Lambda-Funktionen im Vergleich zu gewöhnlichen Funktionen besondere Übersetzungsregeln, welche mit der Priorität von Operatoren (operator precedence) anders interagieren.
Zeile 4 erzeugt einen Syntaxfehler wegen ungültiger Pfeilfunktions-Argumente.
Verwendung von new
Pfeilfunktionen können nicht als Konstruktoren verwendet werden. Sie rufen einen Fehler hervor, wenn auf sie ein new
angewandt wird.
Verwendung von yield
Das yield
-Schlüsselwort sollte im Rumpf einer Pfeilfunktion nicht verwendet werden (außer wenn dies innerhalb von darin weiter verschachtelten Funktionen erlaubt ist). Als Folge können Pfeilfunktionen nicht als Generatoren (siehe weiter unten) verwendet werden.
5.3 Erweiterte Objektliterale
Die Objektliterale wurden so erweitert, dass nun der Prototyp korrekt bestimmt werden kann. Damit sind Klassendefinitionen und die Erzeugung von Instanzen eleganter möglich.
Die Eigenschaft __proto__
(mit zwei Unterstrichen) erfordert native Unterstützung im Browser. Node.js unterstützt dies explizit.
5.4 Destrukturierung
Das Aufbauen von Objekten mittels Mustern erlaubt ein Erstellen komplexer Wertegruppen – ohne Meldungen im Fehlerfall.
Die Beispiel sind jeweils kurz kommentiert. Der Einsatz bietet sich an, um komplexe Rückgabeobjekte schnell als einzelne Variable bereitzustellen.
5.5 Generatoren und Iteratoren
Generatoren und Iteratoren sind elegante Möglichkeiten aufzählbare Objekte zu erstellen und zu nutzen. Die Funktionen gehen weit über Arrays hinaus. Arrays sind quasi die primitivste Form.
Generatoren
Generatoren erstellen Iteratoren. Dazu wird das Schlüsselwort yield
benutzt. Es hält intern einen Zeiger auf einen Wert, der bei jedem Aufruf der Methode durch den Schleifenbefehl (for
, while
) benutzt wird, um den Nachfolger zu finden.
Generator-Funktionen werden mit function*
bezeichnet – das Sternchen ist der entscheidende Teil. Der Aufrufer bekommt ein aufzählbares Objekt. Es ist aufzählbar, weil es über eine Funktion next() verfügt, die aufgerufen wird, um zu prüfen, ob es weitere Elemente gibt. Ist das der Fall, so wird das Objekt, dass next() zurückgibt, im Feld done den Wert false
haben.
Wird eine Objektfunktion benutzt, entfällt das Schlüsselwort function
:
Das Sternchen auf Zeile 2 steht da etwas einsam herum, hat jedoch weiter eine konkrete Aufgabe.
yield
yield
kehrt sofort zurück, der Codefluss endet am aktuellen yield
. Frühere yield
-Erscheinungen werden übersprungen, spätere ignoriert. Der Fluss führt also immer zu einem konkreten yield
.
Sollen alle Elemente zurückgegeben werden, so kann in der Generator-Funktion – und nur dort – auch yield*
geschrieben werden – das Sternchen ist der entscheidende Teil.
Iteratoren
Der Code nutzt eine bestimmte Objektstruktur, die vorgegeben ist. Erreicht wird diese durch den Schlüsselwert Symbol.iterator
. Technisch ist dies eine Fabrik-Funktion, die Instanzen erzeugt. In diesem Fall erzeugt sie ein aufzählbares Objekt. Die Definition ist folgende:
iterator muss nun bestimmten Bedingungen gehorchen, um zu funktionieren. Konkret verlangt wird folgende Struktur:
Der Iterator liefert also mit jedem Schleifendurchlauf eine Funktion next(), deren Rückgabewert zum einen den konkreten Schleifenwert in value liefert, zum anderen eine Information done, die anzeigt, ob es noch weitere Werte gibt. Das letzte steuert den Schleifenabbruch.
Arrays erfüllen diese Vorgaben bereits. Eigene Iteratoren lassen sich zudem wie oben bereits gezeigt mit dem Schlüsselwort yield
erzeugen. yield
hält in der Funktion ein verstecktes Feld, in dem der letzte Abbruch aufbewahrt wird. Kommt es nun bei einem Schleifendurchlauf zum Abruf von Daten, so wird der letzte Wert in diesem Feld analysiert und zum nächsten Wert weitergeschaltet – da erwartete Verhalten von next() tritt auf. Der folgende Abruf zeigt dies auch ohne Schleife:
In Zeile 2 wird der Iterator des Arrays geholt. Dann kann man auf die Werte mit Aufrufen von next() zugreifen.
Iteratoren für Daten
Bereits im Grundlagenkapitel wurde das Schlüsselwort for..of
vorgestellt, das eine Schleife auf einem Iterator bildet. Auch über Set
und Map
kann iteriert werden:
Mehr zu Set
und Map
ist im Kapitel Set und Map zu finden.
````
6 Asynchrone Programmierung
Asynchrone Programmierung ist besonders wichtig, weil JavaScript selbst Single-threaded ist – das heißt nur ein aktiver Thread führt das Skript aus. Wenn in diesem Thread blockierende Vorgänge stattfinden, dann wird das Skript selbst blockiert. Dies hat oft negative Auswirkungen auf die Verfügbarkeit. Der Benutzer kann dies mitbekommen, weil die Oberfläche nicht mehr reagiert oder grafische Effekte nicht mehr gleichmäßig ablaufen.
6.1 Klassische asynchrone Programmierung
Vor allem im Browser ist asynchrone Programmierung wichtig, um dem Bedarf nach einer flüssigen Benutzeroberfläche gerecht zu werden. In der klassischen Programmierung sind dafür Threads im Einsatz. Damit lassen sich Prozesse parallel (tatsächlich scheinbar parallel) ausführen. So lässt sich eine Schaltfläche auch dann noch betätigen, wenn gerade ein Dienstaufruf läuft.
In fast allen bisherigen JavaScript-Versionen basieren auf drei Maßnahmen:
- DOM Ereignisse
- Standard-API Funktionen wie
setTimeout
,setInterval
odersetImmediate
- AJAX-Aufrufe oder XHR-Anfragen
In allen Fällen werden hier browserspezifische Funktionen benutzt, die sich intern um Threads kümmern. Der Entwickler ist dagegen von dieser Umgebung weitgehend abgeschirmt und kann keine eigenen Threads starten. Neuere Entwicklungen versuchen diesen Weg weiterzugehen und einfache Programmiermittel bereitzustellen, die intern Threads nutzen.
Asynchrone Funktionen
Das folgende Listing 6.1 zeigt, wie eine Funktion asynchron ausgeführt werden kann:
Hier wird die Funktion sum, die verhältnismäßig komplex ist und bei großen Datenmengen möglicherweise viele Zeit in Anspruch nimmt, asynchron ausgeführt. Dazu wird ein Funktionszeiger in Zeile 12 übergeben und dann im setTimeout
in Zeile 2 ausgerufen. Die Funktion selbst kehrt sofort zurück und die Ausgabe “Warte…” erscheint. Die Ausgabe in Zeile 9 erscheint dann verzögert. Um dies bei kleinen Datenmengen zu simulieren, wäre eine Angabe einer künstlichen Verzögerung bei setTimeout
in Zeile 2 möglich:
Die Array-Funktionen in diesem Beispiel simulieren nur komplexen Code, sie haben sonst keinen Zusammenhang mit asynchroner Programmierung.
Continuation Passing Style (CPS)
Dieser Stil soll die Schreibweise von asynchronen Funktionen verbessern. Es gibt hier nur wenige Regeln:
- Funktionen geben nichts an den Aufrufer zurück
- Jede Funktion hat eine Rückruffunktion oder eine Fortsetzungsfunktion als Argument
- Die Rückruffunktion oder Fortsetzungsfunktion ist immer die letzte der Argumentliste
Das folgende Beispiel zeigt das Prinzip an Hand einfacher Demofunktionen:
Die Funktionen bilden nun eine Sequenz, die synchron abgearbeitet wird, wie in Abbildung 6.1 gezeigt.
Ein etwas konkreteres Beispiel (Listing 6.2) definiert mehrere Funktionen, die sequenziell ausgeführt werden:
Die Funktionen sind als Lambda-Ausdrücke definiert. Der Vorteil ist die Lesbarkeit und Erkennbarkeit der Reihenfolge. Die erwartete Ausgabe ist “-4”. Folgende Berechnung wird ausgeführt: (a + ((Math.pow(b, 2)) * c) / d) - e
.
Nachteilig bei dieser Art der Stack-Verarbeitung ist die Tatsache, dass Funktionsaufrufe eine Struktur auf dem Stack ablegen. Der Stack ist begrenzt und vor allem rekursive Konstruktionen sind nicht unbegrenzt ausführbar. Die folgende Funktion überfordert den Stack ab dem Wert n=32768:
Wie ist das lösbar? Hier kommt ein weiteres Entwurfsmuster in Betracht, das sich dann wiederum auf CSP stützt. Dies wird als Tail Call (Endaufruf) bezeichnet. Endaufrufe passieren, wenn eine Funktion das Ergebnis einer anderen Funktion zurückgibt. Diese andere Funktion ist in der Endposition (tail). JavaScript, insbesondere seit ES2015, hat dafür eine Optimierung. Die Funktion könnte also besser folgendermaßen geschrieben werden:
Um nun als Rückgabe einen Funktionsaufruf zu erhalten, wird eine innere Funktion _factorial erstellt. Die Optimierung besteht darin, die Aufrufkette des rekursiven Aufrufs nicht als Kette von Stackrahmen abzulegen, die den Stack irgendwann überfordern, sondern nur das finale Ergebnis, denn nur dieses ist schlussendlich interessant.
Nun kann man der JavaScript-Engine weiter behilflich sein, Aufrufe zu optimieren. So ließe sich die letzte Position in der Kette als Schleife schreiben und nicht mehr als rekursiver Aufruf. Der Nachteil kann in der Praxis sein, dass zusätzliche Abbruchbedingungen erstellt werden müssen. Das einfache Fakultät-Beispiel ginge dann auch so:
Das ist freilich auf diesen Fall optimiert, was nicht sinnvoll ist, weil man Fakultäten außer zu Lehrzwecken eher seltener benötigt. Deshalb an dieser Stelle eine universellere Funktion:
Das Verfahren wird trampolining genannt, weil der Aufruf wie auf einem Trampolin abprallt und die Aufrufe nun nacheinander (CSP) erfolgen, ohne sich auf dem Stack zu stapeln. Und so wird es benutzt:
Nun gibt diese Funktion Infinity
zurück, weil 32768! nicht mehr im Wertebereich von number
liegt, aber der Stack-Fehler tritt nicht mehr auf. In anderen Fällen kann das entscheidend sein. Und diese anderen Fälle sind eben vor allem asynchrone Aufrufe, die Rückruffunktionen haben, die dann den Stack belasten.
Fehlerbehandlung
Unerwartet Fehler werden meist mit try..catch
abgefangen. Das ist bei asynchronen Funktionen problematisch, weil der fehlerhafte Funktionsaufruf nicht im selben Thread wir die Fangfunktion ist. Das folgende Beispiel zeigt, wie es nicht aussehen darf:
Der Fehler wird in Zeile 4 mit throw
ausgelöst, aber nicht vom catch
gefangen. Der Thread, indem catch
verarbeitet wird, ist nicht derselbe wie der, indem der Fehler entsteht. Der Fehler bleibt unbehandelt. Wenn die Auswirkungen des Fehlers den Fluss im Haupt-Thread nicht stören, bleibt der Fehler unbemerkt und das Programm verhält sich unvorhersehbar oder instabil. Um das Problem zu lösen, muss der Abfangmechanismus ins Innere transportiert werden:
Um den Weg nach draußen zu finden, wird eine weitere Rückruffunktion für den Fehlerfall angeboten. Das kommt Ihnen bekannt vor? Alle Bibliotheken mit asynchronen Funktionen und auch die noch zu behandelnden neu Promise
-Funktionen nutzen dieses Prinzip. Nun widerspricht das ein wenig dem CSP-Muster. Dort steht die Kettenfunktion an letzter Stelle. Logischerweise erwartet man den positiven Ausgang und deshalb steht in solchen Skripten dann der Fehler als vorletzter Parameter und der Erfolgsfall als letzter.
Da dies häufiger benötigt wird, wäre eine Kapselung möglich:
6.2 Promise
Ein Promise ist eine Funktion, die für das asynchrone Programmieren geeignet ist. Es handelt sich um eine Syntaxvereinfachung durch einen speziellen Typ. Die Komplexität der Thread-Verwaltung wird hier vollständig vor dem Programmierer versteckt. Es handelt sich bei Promises um ein Entwurfsmuster für asynchrone Aufrufe. Es gibt alternative Methoden dazu. Diese dürften obsolet werden, da die Variante Promise/A+ Teil des ES2015 Standards geworden ist.
Natur des Promise
Ein Promise ist ein Standard und eine Implementierung für asynchrone Aufrufe. Es gilt:
- Es muss eine Methode
then
existieren -
then
muss zwei (optionale) Parameter akzeptieren, onFullFilled (Erfolg) und onRejected (Misserfolg) - Der Promise ist unveränderlich (immutable)
- Der Rückgabewert ist garantiert, auch bei nachträglicher Registrierung
Vor allem der letzte Punkt ist interessant. Ereignisse (events) würden sich anders verhalten, auch wenn deren Rückruftechniken dem Promise sehr ähnlich sind. Wird beim Ereignis die Behandlungsfunktion (Rückruf) erst nach dem Aufruf angeschlossen, geht der Aufrufwert verloren. Es können deshalb Wettlaufsituationen (engl. Race Conditions) entstehen, die Aufrufreihenfolge führt zu nichtdeterministischem Verhalten. Promises sind robuster.
Die Idee hinter dem Promise-Objekt ist vor allem die Vereinfachung der Syntax durch das Vermeiden sehr vieler Verschachtelungen durch Rückruffunktionen. Die Auflösungsketten mit then
und bei Bedarf mit catch
werden auf derselben Ebene geschrieben. Dies verbessert die Lesbarkeit. Funktionale Verbesserungen sind nicht zu erwarten. Bevor es Promise
gab, wurde diese Funktion durch Bibliotheken abgedeckt, wo es zu sehr vielen Implementierungs- und Syntaxvarianten kam. Mit der Aufnahme in den Sprachstandard wird dieses Chaos beendet.
Syntax und API
Die folgenden Beispiele zeigen, wie mit Promises gearbeitet werden kann. Der folgende Aufruf wird sofort ausgeführt:
Obwohl der Promise auf Zeile 1 bereits ausgeführt wurde, erfolgt die Ausgabe des Wertes auf Zeile 2.
Hier wird ein Promise benutzt, um eine asynchrone Funktion auszuführen:
Die Ausgabe ist “6” und “4” (in dieser Reihenfolge). Es wurden also zwei Threads gestartet, unabhängig voneinander ausgeführt und die Daten werden getrennt behandelt. Die Addition in Zeile 6 hat keinen Einfluss auf das Ergebnis in Zeile 10.
Promises erzeugen
Das folgende Listing 6.4 zeigt dies (setTimeout
in Zeile 3 simuliert verzögerndes Verhalten).
Hier wird der Konstruktor-Aufruf mit new
benutzt, um ein Promise-Objekt zu erzeugen. Das Basisschema ist einfach:
Im Grunde wird also der Erfolgs- und der Fehlerfall explizit aufgerufen. Der Trick ist hier nicht die Logik – die ist trivial – sondern die Tatsache, dass Promises intern asynchron sind und sich der Entwickler nicht mit diesen Details auseinandersetzen muss.
Wie im letzten Beispiel zu sehen ist, entsteht hier ein Objekt (in der Variablen p). Dies kann als Parameter weitergereicht oder sonstwie aufbewahrt werden. Es kann auch als Rückgabewert einer Funktion dienen. Das folgende Promise gibt den Wert 5 zurück:
Der Ausdruck rechts vom =>
-Operator in Zeile 1 wird asynchron ausgeführt. Das ist der Sinn der Aktion.
Gruppen
Promises lassen sich zu Gruppen zusammenstellen. Nehmen Sie an, eine Anwendung benötigt drei Dienstaufrufe. Ein sinnvolles Fortsetzen ist nur möglich, wenn alle Dienste geantwortet haben. Dafür bietet sich die statische Methode Promise.all()
an. Die Methode nimmt ein Array aus Promise-Objekten auf und kehrt erfolgreich zurück, wenn alle Promise-Objekte zurückgekehrt sind.
Das folgende Beispiel zeigt die Vorgehensweise. $
ist hier das Objekt der Bibliothek jQuery, die den Ajax-Aufruf getJSON
bereitstellt. Die Funktion erzeugt einen Promise:
Mit mehreren Aufrufen entstehen daraus mehrere Promise-Objekte und daraus dann das Array. Wenn einer der Aufrufe fehlschlägt, wird das gesamte Array abgebrochen und in den catch
-Zweig verzweigt.
Die Verarbeitung der einzelnen Ergebnisse erfolgt nur, wenn alle API-Aufrufe erfolgreich waren. Die Methode then
in Zeile 8 wird erst ausgelöst, nachdem der letzte AJAX-Aufruf erfolgreich Daten geliefert hat. Andernfalls erfolgt die Fehlerausgabe. Wenn einige Aufrufe (aber nicht alle) erfolgreich zurückgekehrt sind, werden deren Ergebnisse verworfen.
Nun kann der Fall eintreten, dass diese Teilergebnisse sinnvoll benutzt werden können. Dazu gibt es die statische Methode race
. Der Ablauf entspricht all
, die Methode kehr jedoch zurück, sowie der erste Aufruf erfolgreich war. Die weiteren werden ausgeführt, aber nicht weiter beachtet. Auch deren Fehlerzustände werden ignoriert.
Die Funktion delay verzögert die Ausführung und ruft eine Behandlungsfunktion asynchron auf. Es handelt sich hier, ebenso wie bei fetchJSON, um ein Promise. Beide Aufrufe (Zeilen 8 und 9) starten gleichzeitig. Wenn die API sofort reagiert, wird deren Rückgabewert in Zeile 12 benutzt. Wenn die API jedoch innerhalb der benutzten 5 Sekunden keine Antwort liefert, dann läuft die delay-Funktion ab und deren Wert wird der Rückgabewert, hier also der Wert { user: 'guest' }
.
Fehlerbehandlung
Bei der Fehlerbehandlung wurde bereits die Rückruffunktion im Zweig “rejected” benutzt. Alternativ dazu und damit zu einem zweiten Parameter im then
-Aufruf kann die Methode catch
benutzt werden.
Dies entspricht der Benutzung des zweiten Parameters im then
, weshalb der folgende Code ein identisches Verhalten aufweist:
Der Wert null
in zweiten Zeile erzwingt das Ignorieren des Rückgabewertes und damit die Nutzung des Fehlerwertes.
Das Auslösen erfolgt mit throw
. Wenn Sie also in der inneren Funktion eine Ausnahme auslösen, dann wird automatisch der “reject”-Zweig aufgerufen und damit eine gegebenenfalls vorhandene catch
-Methode erreicht.
6.3 Promise-Arrays
Neu in ES2020 ist die Möglichkeit, ein Array von Promise-Instanzen zu nutzen und die Funktion Promise.allSettled(promise_array)
aufzurufen. Die Funktion kehrt mit einem singulären then
zurück, nachdem alle Promises aufgelöst wurden. Ob dies eine positive (resolve) oder negative (reject) Auflösung ist, spielt keine Rolle. Das Ergebnis (im Erfolgsfall) ist wiederum ein Array mit den einzelnen Ergebnissen in der Reihenfolge der Definition des eingehenden Arrays.
6.4 Async und Await
Asynchrone Aufrufe mit Promise
lassen sich einfacher gestalten, indem die Schlüsselwörter async
und await
benutzt werden.
Async Functions
Asynchrone Funktionen werden durch die Anpassung async
deklariert:
In Zeile 9 sehen Sie den Einsatz. Die Funktion selbst gibt kein Promise
zurück, sondern wird selbst asynchron. Der Aufruf intern sollte deshalb blockieren und auf die Ausführung warten. Dies erfolgt auf Zeile 11 mit await
.
Beschränkungen
Im Grunde ist der Wartezustand an der Stelle, wo ohne diese Syntax auf den Rückruf im then
gewartet wird. Allerdings trifft diese Aussage nur zu, wenn man genau ein then
hat. Eine Kette von then
-Aufrufen, die ohne Wartezeit in einem der Rückruffunktionen quasi parallel ablaufen, ist mit await
nicht darstellbar. Der Einsatz von async
und await
ist nicht nur eine Vereinfachung der Schreibweise, sondern auch einfachen Einsatzfällen vorbehalten.
7 Aufzählbare Typen
Set
und Map
speichern Datenstrukturen mit bestimmten Rahmenbedingungen. Verschiedene Array-Varianten für spezifische Einsätze helfen bei der Verarbeitung binärer Daten. Das ArrayBuffer
-Objekt repräsentiert einen generischen Buffer mit fester Länge.
7.1 Set
Sets sind Sammlungen von Werten, über deren Elemente in der Reihenfolge des Einfügens iteriert werden kann. Ein Wert kann in einem Set nur einmal erscheinen; er ist im Set eindeutig. Die Eindeutigkeit entspricht nicht zwingend ===, sondern eher wird der Symbolform gefolgt. NaN
als Wert wird erkannt, obwohl NaN !== NaN
ist.
Mit Set
lassen sich eindeutige Werte jedes beliebigen Typs speichern, egal ob es sich dabei um primitive Werte oder Objektreferenzen handelt. Übergeben werden muss beim Erstellen ein iterierbares Objekt. Meist handelt es sich dabei um ein Array.
Die Funktionen, die ein Set
-Objekt bietet, sind:
-
Set.prototype.add(value)
: Hängt ein Element an -
Set.prototype.clear()
: Entfernt alle Elemente -
Set.prototype.delete(value)
: Entfernt ein Element mit dem angegebenen Wert, gibt den Wert aber dennoch zurück -
Set.prototype.entries()
: Ergibt einen Iterator über die Werte -
Set.prototype.forEach(callbackFn[, thisArg])
: Ruft eine Funktion für alle Elemente auf -
Set.prototype.has(value)
: Prüft auf einen Wert -
Set.prototype.keys()
: Ein Alias fürvalues()
-
Set.prototype.values()
: Die Werte in der Reihenfolge, in der sie eingefügt wurden
Set verfügt nicht über typische Array-Funktionen wie map
oder reduce
. Bei kleinen Datenmengen eignet sich folgendes Konstrukt:
Hier wird das Set mySet mit dem Spread-Operator in ein Array konvertiert und dann darauf eine Array-Funktion (hier map
) benutzt. Der Nachteil ist klar der Speicherverbrauch und Allokierungskosten.
Weitere Methoden sind:
-
add(val)
: Eintrag am Ende einfügen -
clear()
: Alles löschen -
delete(val)
: Einen Eintrag löschen -
entries()
: Erzeugt einen neuen Iterator mit Schlüssel-/Werte-Paaren, wobei hier Schlüssel gleich Wert ist -
forEach(callback [, args])
: Führt eine Funktion für jeden Eintrag aus -
has(key)
: Prüft auf einen Eintrag -
values()
: Einfache Liste der Einträge als Array -
keys()
: Ein Alias fürvalues()
[@@iterator]()
Es gibt nur wenige Eigenschaften:
-
size
: Anzahl der Elemente -
get Set[@@species]
: Das Konstruktor-Symbol (entsprichtnew Set()
)
7.2 Map
Map ist dem Set sehr ähnlich, nur dass die Elemente hier Schlüssel-Werte-Paare sind.
Weitere Methoden sind:
-
clear()
: Alles löschen -
delete(key)
: Einen Eintrag löschen -
entries()
: Erzeugt einen neuen Iterator mit Schlüssel-/Werte-Paaren -
forEach(callback [, args])
: Führt eine Funktion für jeden Eintrag aus -
get(key)
: Ruft einen Eintrag nach dem Schlüssel ab -
has(key)
: Prüft, ob ein Schlüssel vorhanden ist -
keys()
: Alle Schlüssel als Array -
set(key, val)
: Ändert einen Eintrag -
values()
: Iterator der Werte -
[@@iterator]()
: Allgemeiner Iterator mit Schlüssel-/Werte-Paaren
Es gibt nur wenige Eigenschaften:
size
[@@toStringTag]
get Map[@@species]
Schlüsselvergleiche
Sehr speziell ist der Schlüsselvergleich in Map. Der Schlüsselvergleich basiert auf einem echten String-Vergleich: NaN wird als gleich mit einer anderen NaN angesehen, obwohl immer noch gilt, dass NaN !== NaN
, das heißt ungleich zu allem. Alle anderen Werte müssen identisch im Typ und Wert sein. Dies entspricht der Benutzung des Operators ===. In der aktuellen ECMAScript-Spezifikation werden auch -0 und +0 als gleich behandelt, auch wenn dies in früheren Entwürfen anders war. Dies ist zumindest fragwürdig in Bezug auf die innere Darstellung.
7.3 Iteratoren
Die Schleife for..of
iteriert über Set- und Map-Objekte und berücksichtigt generell nur Eigenschaften. Das ist der wesentliche Unterschied zu for..in
, wo alle Wert berücksichtigt werden.
Der Iterator kann direkt abgerufen werden, um ein aufzählbares Objekt zu erhalten, dass mit for..of
auswertbar ist:
Die Ausgabe lautet:
Symbol.iterator
ruft den Iterator ab.
Die Schleife lässt sich auch benutzen, um die Objekte gleich in Variablen zu zerlegen:
Das Array [key, value] wird hier destrukturiert. Die Auswertung der Bestandteile ist dann möglich. Dies gilt sinnvollerweise nur für Map
. Beim Set
liegt generell nur ein Wert der Liste vor.
7.4 WeakMap und WeakSet
Zwei spezielle Versionen existieren mit WeakMap
und WeakSet
. Beide bieten die Möglichkeit, das Schlüssel oder Werte vom Garbage-Kollektor entfernt werden, wenn sie keinen anderen Referenzen mehr aufweisen. Ansonsten würde es vorkommen, dass alleine das Speichern eines Objekts in einem Set
- oder Map
-Objekt dazu führt, dass der Speicher nicht mehr aufgeräumt werden kann, obwohl niemand diese Objekte mehr nutzt.
7.5 ArrayBuffer
Das ArrayBuffer
-Objekt repräsentiert einen generischen Buffer mit fester Länge. Der Inhalt eines ArrayBuffers kann nicht direkt bearbeitet werden; stattdessen wird eines der typisierten Array-Objekte (siehe nächster Abschnitt) oder ein DataView
-Objekt verwendet, welches den Buffer in einem bestimmten Format repräsentiert und von welchem aus sein Inhalt bearbeitet werden kann.
Eine Anwendungsmöglichkeit besteht im Zusammenhang mit fetch
. Dies ist die HTML 5 API zum Laden von Daten via HTTP und mit Hilfe eines Promise
. Die API ist nicht Teil von JavaScript, weshalb sie hier nur am Rande erwähnt wird, aber das erzeugte Antwortobjekt hat einige Methoden zum Aufbereiten der Daten aus dem HTTP-Abruf. Ist eine Weiterverarbeitung binärer Daten erforderlich, bietet sich response.arrayBuffer() an.
Wird die Schreibweise mit then
benutzt, sieht Zeile 5 folgendermaßen aus:
Diverse Media-APIs in HTML 5 (Audio, Video, Image etc.) können einen solchen Buffer direkt verarbeiten. Das ist deutlich effizienter als die klassische Arrays.
API
Folgende Eigenschaften gibt es:
-
length
: Der Wert des ArrayBuffer Konstruktors für die Länge. -
byteLength
: Schreibgeschützter Wert der Größe des Arrays in Bytes. Dieser wird bei der Erstellung des Arrays ermittelt und kann nicht geändert werden.
Des weiteres sind diese Methoden verfügbar:
-
isView(arg)
: Gibttrue
zurück wenn arg eines der Views des ArrayBuffers ist, wie zum Beispiel die typisierten Array-Objekte oder einDataView
. Ansonsten wird false zurückgegeben. -
transfer(oldBuffer [, newByteLength])
: Gibt einen neuenArrayBuffer
zurück, dessen Inhalt von den Daten des oldBuffers genommen wird und dann entweder abgeschnitten oder mit null aufnewByteLength
erweitert wird. -
slice(begin, end)
: Gibt einen neuenArrayBuffer
zurück, welcher eine Kopie der Bytes des eigentlichen ArrayBuffer einthält. Die Kopie geht von begin (inklusiv) bis end (exclusiv). Wenn einer der Werte negativ ist, referenziert er auf den Index vom Ende des Arrays an und nicht vom Beginn des Arrays.
7.6 Typisierte Arrays
Typisierte Arrays enthalten keine Objekte, sondern niedere Skalare wie Bytes, Int32 usw. Auch wenn es diese Typen in JavaScript nicht für Variablen gibt, haben sie in Arrays eine wichtige Funktion. Sie erlauben den effizienten Umgang mit binären Daten. In vielen Fällen wird ein solches Array aus einem empfangenen ArrayBuffer
befüllt. Dieser Type wurde bereits im vorhergehenden Abschnitt vorgestellt.
Übersicht
Folgende spezialisierte Typen stehen zur Verfügung:
-
Float32Array
: Gleitkommazahlen im 4 Byte-Format -
Float64Array
: Gleitkommazahlen im 8 Byte-Format -
BigInt64Array
: BigInt im 4 Byte-Format -
BigUint64Array
: BigInt im 8 Byte-Format -
Int8Array
: Int im 1 Byte-Format -
Int16Array
: Int im 2 Byte-Format -
Int32Array
: Int im 4 Byte-Format -
Uint8Array
: Int im 1 Byte-Format, vorzeichenlos -
Uint16Array
: Int im 2 Byte-Format, vorzeichenlos -
Uint32Array
: Int im 4 Byte-Format, vorzeichenlos -
Uint8ClampedArray
: WieUint16Array
, aber Werte außerhalb des Wertbereiches werden zu 0 oder 255
Alle Array-Typen basieren und TypedArray
und haben damit praktisch dieselbe API.
-
from()
: Statische Methode zum Erzeugen aus einem Array -
of()
: Statische Methode zum Erzeugen aus einzelnen Elementen
Alle anderen Methoden sind Instanzmethoden:
-
copyWithin()
: Kopiert Elemente innerhalb des Arrays -
entries()
: Erzeugt eine Iterator mit Index-/Wertepaaren -
every()
: Prüft eine Bedingung für jedes Element (boolesch) -
fill()
: Füllt das Array -
filter()
: Filtert anhand einer Bedingung -
find()
: Sucht Elemente anhand einer Bedingung -
findIndex()
: Sucht Elemente und gibt den Index der ersten Fundstelle anhand einer Bedingung -
forEach()
: Durchläuft alle Elemente -
includes()
: Prüft, ob es ein bestimmtes Element gibt (boolesch) -
indexOf()
: Prüft, ob es ein bestimmtes Element gibt und gibt dessen Index zurück -
join()
: Konvertiert in eine Zeichenkette -
keys()
: : Erzeugt eine Iterator mit Indexwerten -
lastIndexOf()
: Prüft, ob es ein bestimmtes Element gibt und gibt den letzten gefundenen Index zurück -
map()
: Bearbeitet jedes Element und gibt ein neues Array mit den Ergebnisse zurück -
reduce()
: Wendet eine Aggregationsfunktion an -
reduceRight()
: Wendet eine Aggregationsfunktion an, beginnt aber am Ende -
reverse()
: Dreht das Array um -
set()
: Setzt einen Wert -
slice()
: Extrahiert einen Teil -
some()
: Prüft eine Bedingung für jedes Element und ist true, wenn wenigstens eins gültig ist (boolesch) -
sort()
: Sortiert die Elemente -
subarray()
: Extrahiert einen Teil -
toLocaleString()
: Zeichenkettenform unter Berücksichtigung von Lokalisierunginformationen -
toString()
: Zeichenkettenform -
values()
: Erzeugt eine Iterator mit Werten -
[@@iterator]()
: Der Iterator selbst, beispielsweise für einefor..of
-Schleife
Das sieht allgemein aus wie bei den klassischen Arrays und sollte keine Probleme bei der Anwendung bereiten.
Beispiele
Der Code zum Erzeugen eines Arrays könnte dann folgendermaßen aussehen:
Dies unterstellt, das in dem buffer Informationen sind, die sich als 32 bit Integer abbilden lassen.
8 Reguläre Ausdrücke
Reguläre Ausdrücke haben eine große Bedeutung in JavaScript, da in fast allen Fällen die Erstverarbeitung von Daten durch HTML-Elemente, beispielsweise Formular-Elemente, beginnt. Und HTML liefert nur Zeichenketten. Mit regulären Ausdrücken lassen sich Suchmuster abbilden. Es werden also auf äußerst effektive Weise Zeichenketten durchsucht, analysiert und Teile gegebenenfalls ersetzt.
8.1 Einführung
Um ein Gefühl für reguläre Ausdrücke zu bekommen, will ich Ihnen gleich ein Beispiel präsentieren, dass sicher bereits tausendfach zur Anwendung gekommen ist. Bei der Eingabe von Kundendaten wird oft die E-Mail-Adresse verlangt. Um Fehleingaben zu vermeiden, wäre eine Prüfung der Adresse auf korrekte Schreibweise sinnvoll. Eine Möglichkeit ist die Zerlegung der Adresse in ihre Bestandteile (vor und hinter dem @-Zeichen), die Analyse der Punkte und die Berechnung der Länge der Zeichen nach dem Punkt (dort steht die Toplevel_Domain). Das ist mit ein paar Schleifen und Abfragen sicher gut zu erledigen. Oder mit einem regulären Ausdruck:
Alles klar? Sicher kann niemand, der zum ersten Mal mit regulären Ausdrücken in Berührung kommt, diesen Ausdruck sofort lesen.
Der Sinn eines solchen Musters ist die Erkennung von Textteilen (Zeichenkette genannt) in einem Text. Reguläre Ausdrücke dienen als Vergleichsmuster für solche Zeichenketten. Der gesamte Ausdruck, verpackt in eine Skript- oder Programmiersprache, gibt dann true
oder false
zurück, je nachdem, ob das Muster gefunden wurde oder nicht. In der Praxis sind solche Konstrukte immer in einem bestimmten Kontext zu sehen. In JavaScript würden Sie dies so anwenden:
Hier wird der Ausdruck in die bei JavaScript typischen Begrenzungszeichen /Ausdruck/
eingebaut und der Vergleich erfolgt mit der Funktion match
. Beachten Sie, dass dies ein Literal in JavaScript ist – nicht eine Zeichenkette.
8.2 Kopieren oder Konstruieren?
Im Kapitel “Musterausdrücke” gehe ich umfangreich auf praktische Ausdrücke ein. Der Umfang ist so gewählt, dass Sie nicht nur trickreiche Ausdrücke kennenlernen, sondern auch das eine oder andere Problem durch simples Abschreiben lösen können. Weil das ungewöhnliche Fehlerquellen birgt können Sie sich auf der Website zum Bändchen alle Beispiele herunterkopieren.
Wenn Sie professionell mit JavaScript arbeiten, sollten Sie dennoch versuchen, die Ausdrücke vollständig zu verstehen und gelegentlich selbst welche zu erstellen.
8.3 Und wie funktioniert das?
Sicher sind Sie nun neugierig, wie der reguläre Ausdruck im letzten Beispiel funktioniert.
Wenn Sie Ausdrücke dieser Art analysieren, sollten Sie zuerst die Sonderzeichen erkennen und extrahieren. Die folgenden Sonderzeichen werden hier verwendet: ^, $, +, *, ?, [], ()
. Alle anderen Zeichen haben in diesem Zusammenhang keine besondere Bedeutung (das sind nicht sehr viele). Hier eine Übersicht über die Bedeutung:
* ^
legt den Suchbegriff an den Anfang des Musters. Wenn Sie ^x schreiben, wird das x also nur am Anfang der Zeichenkette gefunden werden.
* $
steht für die Platzierung am Ende des Suchbegriffs.
* *
bezeichnet kein oder eine beliebige Anzahl von Zeichen, wobei es sich immer auf das Zeichen davor bezieht.
* +
steht für mindestens ein oder beliebig viele Zeichen.
* ?
dagegen bezeichnet kein oder genau ein Zeichen.
* [a-z]
definiert ein Zeichen aus einer Zeichengruppe. Im Beispiel dürfen alle Kleinbuchstaben auftreten (keine Ziffern, aber auch keine Großbuchstaben).
* ()
gruppiert Zeichen oder Zeichenfolgen. Die Mengenoperatoren wie * oder + können auch auf Gruppen angewendet werden.
* {}
markieren freie Wiederholungsdefinitionen.
* \
(Backslash) markiert Metazeichen oder maskiert die Sonderzeichen, so dass sie ihre Symbolik verlieren. Der Punkt .
steht für genau ein beliebiges Zeichen (wird hier nicht verwendet), mit \.
ist aber tatsächlich der Punkt gemeint (der wird hier benötigt).
Jetzt können Sie den Ausdruck schon gut zerlegen. Das @ steht offensichtlich für sich selbst, der Ausdruck besteht also aus zwei Teilen, einer vor und einer nach dem @:
Der erste Teil steht vor dem @
-Zeichen und muss mindestens ein Zeichen enthalten. Die erste Zeichengruppe [_a-zA-Z0-9-]
definiert die zulässigen Zeichen und erzwingt mit dem +
mindestens ein Zeichen. E-Mail-Namen dürfen aber auch Punkte enthalten, nur nicht an erster Stelle. Der Ausdruck kann also auch mit einem Punkt, gefolgt von weiteren Zeichen, fortgesetzt werden. (\.[_a-zA-Z0-9-]+)
definiert solche Folgen aus einem Punkt und wiederum beliebig vielen (aber mindestens einem) Zeichen. Die ganze so definierte Gruppe (erkennbar an den runden Klammern) ist optional (0-mal) oder beliebig oft zu verwenden. Der zweite Teil besteht aus Zeichenfolgen (diesmal ohne den Unterstrich, der ist in Domainnamen nicht erlaubt). Dann muss ein Punkt folgen (deshalb folgt kein Mengenoperator), die Toplevel-Domain besteht aus Buchstaben und kann nur zwei oder drei Zeichen lang sein.
Auflösungshilfen
Der Ausdruck mag Ihnen unheimlich erscheinen, aber ist keineswegs perfekt (es gibt zulässige Fälle, die hier durchs Raster fallen). Er ist vor allem aber in dieser Form schwer erklärbar. Da noch größere Herausforderungen vor Ihnen liegen, möchte ich eine andere Form der Darstellung wählen:
Das war doch schon leichter lesbar. Leider können Sie das so in JavaScript nicht schreiben. Ich werden diese Form deshalb nur verwenden, um besonders verzwickte Ausdrücke aufzulösen. Wenn Sie mit einem Ausdruck nicht zurechtkommen, den ich im Buch verwende und nicht ausreichend erkläre, versuchen Sie diesen Ausdruck so aufzulösen.
8.4 Muster erkennen
In diesem Kapitel werden die grundlegenden Techniken anhand besonders einfacher Beispiel gezeigt. Dies genügt noch keinen praktischen Anforderungen, eignet sich aber gut als Grundlage eigener Experimente.
Grundlagen
Was passiert bei Auswerten eines Ausdrucks wirklich? Praktisch geht es immer darum, einen Text innerhalb eines umfangreicheren Textes zu suchen (Zeichenkette, Datei, Datenbank). Das ist aber nicht der eigentliche Anwendungsfall, denn so etwas erledigen Funktionen wie search
(Suche einer Zeichenkette in einer anderen) effizienter. Reguläre Ausdrücke definieren Eigenschaften eines Suchtextes. Damit kann das Muster nicht nur auf einen bestimmten Suchtext passen, sondern auf eine ganze Reihe von Varianten. Suchen sowie Suchen und Ersetzen gewinnen so eine ganz andere Bedeutung.
Beim Suchen wird der reguläre Ausdruck genutzt, um eine Übereinstimmung festzustellen. Im Kontext einer Skriptsprache gibt der vollständige Ausdruck true
oder false
zurück. Da solche Vorgänge umfangreich sein können, werden manche reguläre Ausdrücke auch in Gruppen zerlegt. Dann gibt die Funktion auch die gefundenen Teile zurück. Diese wiederum können in weiteren Teilen desselben Ausdrucks oder später im Programm erneut verwendet werden. Auf diese sogenannten Referenzen gehe ich später detailliert ein.
Um die Darstellungen in diesem Buch möglichst kompakt zu halten, werde ich im folgenden Abschnitt einige Begriffe einführen, die immer wieder benötigt werden. Wenn Sie im Umgang mit diesen Begriffen vertraut sind, können Sie den Abschnitt überspringen.
Zeichen, Zeilen und Texte
Es wurden bereits Begriffe wie Zeichen, Zeile und Text verwendet. Wichtig ist das Verständnis des Begriffs Zeile. Zeilen enden mit einem Zeilenumbruch (da wo Sie in der Textverarbeitung oder im Editor Enter drücken). Viele Begrenzungszeichen reagieren auf Zeilenbegrenzungen. Dateien werden häufig auch zeilenweise eingelesen. Bei der Definition von Ausdrücken müssen Sie also darauf in spezieller Weise reagieren, sonst werden Muster, die Zeilengrenzen überschreiten sollen, nicht erkannt.
Begriffe für reguläre Ausdrücke
In diesem Abschnitt werden einige weitere Begriffe eingeführt.
Metazeichen
Innerhalb eines regulären Ausdrucks können bestimmte Zustände in einer besonderen Weise gekennzeichnet werden. So wird der Beginn einer Zeichenkette mit ^
gekennzeichnet, das Ende dagegen mit $
. ^
und $
sind also Metazeichen. Wenn Sie Metazeichen dagegen suchen möchten, müssen Sie ein Backslash davor stellen:
* \$
, \^
: Der Backslash hebt die besondere Wirkung des Metazeichens auf. Auch der Backslash selbst ist ein Metazeichen. Um danach zu suchen, schreiben Sie: \\
.
Alle Metazeichen werden noch genau erklärt.
Literale
In JavaScript können Sie reguläre Ausdrücke in Literale schreiben. Solche Ausdrücke sind Teil der Sprache und müssen nicht aus Zeichenketten extrahiert werden. Das Literal ist der Schrägstrich /
:
/[abc]/
Sie können dies als Argument in Funktionen nutzen oder einer Variablen zuweisen. Sie können auch Funktionen darauf anwenden:
Zeichenklassen
Wenn Sie nach Zeichenfolgen oder Zeichen suchen, ist es oft effizienter, eine Zeichenklasse anzugeben. Zeichenklassen werden mit eckigen Klammern [abc]
markiert, Dabei steht die gesamte Definition für ein Zeichen. Ein Wiederholungsoperator gibt an, ob und wie oft das Zeichen oder ein Zeichen aus der Klasse auftreten darf.
Zeichenklassendefinitionen werden durch Auflistung von zulässigen Zeichen gebildet, wobei auch Gruppen zulässig sind.
Referenzen
Teile regulärer Ausdrücke werden in temporären Speicherstellen abgelegt. Auf diese können Sie sich später im Ausdruck beziehen. Dadurch sind sehr komplexe Wiederholungen leicht zu realisieren. Referenzen nutzen den Backslash und eine Referenznummer (\4
).
Metazeichen
Hier nun eine Übersicht über die Benutzung der Metazeichen.
Anfang, Ende und Grenzen
Die wichtigsten beiden Metazeichen kennen Sie schon aus den Grundlagen:
* ^
markiert den Beginn der Zeichenkette
* $
bezeichnet dagegen das Ende
-
\b
steht für eine Wortgrenze ohne Inanspruchnahme eines Zeichens -
\B
steht für keine Wortgrenze ohne Inanspruchnahme eines Zeichens
Wie ist das zu verstehen? Wenn Sie nach dem Wort “Auto” suchen, dann muss das “A” am Anfang stehen, das “o” dagegen am Ende. Sie würden einen solchen Ausdruck also ^Auto$
schreiben. Darf das Wort “Auto” dagegen irgendwo im durchsuchten Text auftreten, setzen Sie die Metazeichen ^
und $
nicht.
Das Ende ist meist das Ende der Zeile. Dies gilt nicht, wenn in umgebenden Schaltern andere Bedingungen eingestellt werden. Darauf gehe ich in Kapitel 3 ein.
Wortgrenzen sind Übergänge zu einem Wort. Als Wortgrenze zählt ein Leerzeichen, Komma, Punkt usw. Das spezielle Symbol \b
reagiert darauf, beschreibt jedoch keine Zeichenstelle, sondern quasi den “Bereich” zwischen dem Leerzeichen und dem ersten Wortzeichen.
/\bko
erkennt “ko” in dem Satz “Das ist kompliziert.”. Das Leerzeichen vor dem “ko…” erzeugt die Wortgrenze.
Ein beliebiges Zeichen
Oft wird an einer bestimmten Stelle ein Zeichen erwartet, egal um welches es sich handelt. Es wäre mühevoll, immer mit dem gesamten Zeichenvorrat zu operieren, deshalb gibt es ein spezielles Metazeichen dafür:
• .
steht für genau ein beliebiges Zeichen
Sie könnten nun “Auto” in verschiedenen Kontexten suchen und dazu so vorgehen:
• .uto
findet “Spielzeugauto”, “Auto”, “Automatik” usw., aber auch “Distributor” (was sicher nicht immer erwünscht ist).
Ohne Zeichen
Angenommen Sie suchen leere Zeilen in einer Datei, dann sollte der folgende Ausdruck dies erledigen:
So einfach? Sicher, die Zeile hat einen Anfang (den hat jede Zeile), der mit ^ erkannt wird, dem Anfang folgt unmittelbar das Ende $, dazwischen ist nichts, also muss die Zeile leer sein.
Sinnlos ist dagegen ein einzelnes ^
als Suchmuster. Dies markiert einen Zeilenanfang, diese Bedingung trifft aber auf jede Zeile zu.
Zeichenklassen
Zeichenklassen definieren Gruppen von Zeichen.
Einer aus vielen
Zeichenklassen werden durch eckige Klammern markiert. Ohne weitere Metazeichen oder Wiederholungsoperatoren wird nur ein Zeichen aus der definierten Klasse erkannt. Zeichenklassen können durch bloßes Aufzählen oder durch Gruppenbildung definiert werden, letzteres durch das Minuszeichen:
* [aeiou]
definiert einen Vokal
* [a-f]
definiert die Buchstaben a, b, c, d, e, f (als Kleinbuchstaben)
* [a-fA-F0-9]
definiert zulässige Zeichen für Hexadezimalziffern
Die Reihenfolge spielt nur bei Gruppen ein Rolle. Ansonsten ist [a-fA-F]
und [A-Fa-f]
identisch.
Negation
Die gesamte Definition kann negiert werden, wenn am Anfang das Zeichen ^ gestellt wird. Sie sehen richtig, das Zirkumflex hat in der Zeichenklassendefinition eine andere Bedeutung (ist auch ein anderer Kontext). Das macht reguläre Ausdrücke so tückisch, aber auch so spannend.
* [^0-9]
Alles außer Ziffern (also auch Zeichen wie “#”, “*” oder “%”)
* [^aeiou]
ist eine primitive Form der Definition von Konsonanten.
Diese besondere Bedeutung hat das ^
allerdings nur, wenn es unmittelbar der öffnenden Klammer folgt. Die folgende Form entspricht lediglich ein paar speziellen Zeichen:
* [!"§$%^&/()=]
erkennt Zeichen, die auf der PC-Tastatur auf den Zifferntasten liegen.
Ziffern
Häufig wird nach Ziffern gesucht. Diese können mit oder ohne Vorzeichen und Dezimalpunkt oder -komma geschrieben werden. Folgende Definitionen bieten sich dazu an:
* [0-7]
Ziffern der Oktalzahlen
* [0-9+-,]
Dezimalzahlen mit Komma und Vorzeichen
* [a-fA-F0-9]
Hexadezimalziffern
Datum und Zeit
Für Datums- und Zeitangaben wären folgende Definitionen möglich:
* [0-9.]
Datum
* [0-9:]
Zeit
* [0-9:amp]
Englische Zeitangaben mit “am” oder “pm”.
Das letzte Beispiel zeigt, das diese Klasse nicht so sinnvoll ist, denn theoretisch wären auch Zeitangaben wie “a9” oder “p0m” zulässig.
Zeichenketten
Bei Zeichenketten kann man mit Zeichenklassen gezielt auf Groß- und Kleinschreibung reagieren:
* [gG]rün
, [rR]ot
Wenn die Schreibweise eines Namens nicht eindeutig ist, kann man darauf folgendermaßen reagieren:
* M[ae][iy]er
Dieser Ausdruck passt auf Meyer, Mayer, Maier und Meier.
Abkürzungen
Die folgende Tabelle gibt eine Überblick über die unterstützten Metazeichen:
Abkürzung | Beschreibung |
---|---|
\t | Tabulatorzeichen |
\n | Newline (Neue Zeile) |
\r | Return (Wagenrücklauf) |
\f | Formfeed (Seitenvorschub) |
\v | Vertikaler Tabulator |
\s | White-Space (eines der im Druck nicht sichtbaren |
Zeichen, also \t, Leerzeichen, \n, \r, \f) | |
S | Negation von \s |
\w | Wortzeichen (Zeichen, aus denen Wörter bestehen, |
konkret [_a-zA-Z0-9] | |
W | Die Negation von \w |
\d | Ziffer (engl. digit), entspricht [0-9] |
D | Negation von \d |
\b | Wortgrenze, als Anfang oder Ende eines Wortes |
zählen alle Zeichen, die nicht zur Abkürzung \w gehören. | |
B | Negation der Anweisung \b |
\0 | Null-Zeichen (physische 0) |
\xxx | Zeichenwert, dargestellt durch eine oktale Zahl |
\xdd | Zeichenwert in der hexadezimalen Form |
\uxxxx | Unicode-Zeichen in hexadezimaler Schreibweise |
\cxxx | Steuerzeichen, ASCII-Wert |
Wiederholungsoperatoren
Die bisher gezeigten Definitionen sind immer auf ein Zeichen bezogen. Wichtiger ist dagegen die Auswahl einer bestimmten Anzahl Zeichen. Dazu werden Wiederholungsoperatoren eingesetzt. Einige spezielle will ich wegen der besonderen Häufigkeit in regulären Ausdrücken zuerst zeigen. Eine allgemeinere Definition folgt danach.
* a*
definiert kein oder eine beliebige Anzahl von “a”, also “”, “aaaa” usw.
* a+
definiert ein oder eine beliebige Anzahl von “a”, also “a”, “aa” usw.
* a?
definiert kein oder ein “a”, also “” oder “a” usw.
Allgemeine Wiederholungsoperatoren
Neben der allgemeinen Aussage, ob Zeichen erlaubt sind oder nicht, wird in vielen Fällen eine bestimmte Anzahl erwartet. Dazu wird hinter das Zeichen, die Zeichenklassendefinition oder die Zeichengruppe folgender Ausdruck gesetzt:
* {min, max}
Dabei bezeichnet min die minimale Anzahl Zeichen, die erforderlich sind, max dagegen die maximale Anzahl.
* {wert}
Der Ausdruck muss genau die angegebene Anzahl Zeichen haben.
Dieser Ausdruck hat folgende Eigenschaften:
* {,max}
Die minimale Angabe kann entfallen.
* {min,}
Die maximale Angabe kann entfallen.
* {,}
beide Angaben entfallen (siehe nächster Absatz).
Die bereits gezeigten Metazeichen kann man mit dem allgemeinen Wiederholungsoperator auch darstellen. Das ist logisch, denn die Zeichen *, + und ? sind nur Abkürzungen der folgenden Ausdrücke:
-
{,}
entspricht * -
{0,1}
entspricht ? -
{1,}
entspricht +
Wiederholungsoperatoren eignen sich hervorragend zur Bestimmung korrekter Längen bei Eingabewerten. Angenommen Sie haben ein Formular mit einem Feld “postleitzahl”:
Dann wäre folgender Ausdruck geeignet:
Auf einen Blick
Operator | Bedeutung | Beschreibung |
---|---|---|
? | 0 – 1 | Kein oder ein Zeichen |
* | 0 – ∞ | Kein oder beliebig viele Zeichen |
+ | 1 – ∞ | Mindestens ein oder beliebig viele Zeichen |
Zahl | Genau zahl Zeichen | |
Min – ∞ | Mindestens min Zeichen | |
0 – Max | Kein oder maximal max Zeichen | |
Min – Max | Minimal min bis maximal max Zeichen |
Referenzen
Die bisherigen Elemente sollten keine großen Probleme bereitet haben. Für komplexere Auswertungen sind Kombinationen aus Metazeichen und Zeichenklassen nicht ausreichend flexibel. Auf Schleifen zum Durchsuchen von längeren Zeichenketten mit Wortwiederholungen könnten Sie noch nicht verzichten.
Solche Wiederholungen werden in regulären Ausdrücken durch Referenzen erreicht. Praktisch wird damit durch ein besonderes Metazeichen \1
, \2
usw., auf vorher markierte Teile verwiesen. Die Markierung selbst erfolgt durch runde Klammern (…).
Besondere Probleme bereitet dabei immer wieder die Zählung der Klammern, besonders wenn alle Arten von Klammern vielfältig verschachtelt werden. Der Regex-Compiler geht dabei ganz pragmatisch vor. Es werden nur die öffnenden (linken) Klammern durchlaufend gezählt. Die erste öffnende Klammer wird also dem Metazeichen \1
zugewiesen, die zweite \2
usw. Die meisten Umgebungen erlauben bis zu neun Referenzen, einige Implementierungen (wie die von JavaScript) aber auch bis zu 99.
8.5 Gruppierungen
Die Gruppierung alleine kann auch benutzt werden, um die Wiederholungsoperatoren auf mehrere zusammenhängende Zeichen anzuwenden. Die Referenz ist dann quasi ein Nebeneffekt, auf den man Bezug nehmen kann oder nicht.
Einfache Gruppen
Wenn Sie eine bestimmte Zeichenfolge wiederholen möchten, genügt der Einsatz der Gruppierung:
* (ab)+
findet Treffer in “abc”, “abcabc” usw, aber nicht “aacbb”
Umschließende Zeichen
Eine gute Anwendung ist die Suche nach Ausdrücken, die mit einem Zeichen umschlossen sind. Das kommt in Programmtexten häufig vor. Der folgende Ausdruck sucht Worte heraus, die in Anführungszeichen stehen, dabei soll folgende Regel gelten:
* “Wort Wort Wort” wird gefunden
* “Wort Wort Wort’ wird nicht gefunden
* ‘Wort Wort Wort’ wird gefunden
Eine mögliche Lösung wäre:
Wie funktioniert das? Am Anfang der Zeichenkette ^
steht ein “ oder ein ‘ ["']
. Diese Zeichenklasse darf nur einmal auftreten {1}
. Dann können beliebig viele Zeichen .*
folgen, bis am Ende $
wieder das Anführungszeichen vom Anfang steht \1
. Ohne Referenzen wäre das kaum lösbar.
Das folgende Beispiel sucht einfach doppelte Wörter im Text:
Der Ausdruck beginnt mit einer Wortgrenze \b, dann folgt der sich insgesamt wiederholende Ausdruck ((..)..)+
. Der Inhalt ist getrennt zu betrachten. Die Folge (\w+)\s+
erkennt Wörter, die durch Leerräume getrennt sind. Dabei darf ein Whitespace nur auf ein ganzes Wort folgen. Damit das doppelte Wort erkannt wird, nimmt der Ausdruck Bezug auf die Wortdefinition. Diese befindet sich in der zweiten öffnenden Klammer, also wird \2
verwendet.
8.6 Vorwärtsreferenzen
Es kommt vor, dass Ausdrücke nur dann erkannt werden sollen, wenn bestimmte Zeichen davor oder danach erscheinen. Diese Zeichen davor oder danach sind jedoch selbst nicht von Interesse. Der Ausdruck wird also etwas um die Suchstelle “herum” suchen. Sie erreichen dies mit den folgenden Kombinationen:
-
?=
Prüft die Zeichenkette, wenn die nachfolgende passt -
?!
Prüft die Zeichenkette, wenn die nachfolgende nicht passt
In diesem Beispiel werden ein- bis dreistellige Zahlen nur erkannt, wenn ein “d” folgt.
8.7 Die JavaScript-Funktionen
Javascript stellt ein Objekt RegExp
bereit. Das Objekt entsteht entweder durch den new
-Operator oder das //
-Literal. Außerdem kennen einige Zeichenkettenfunktionen reguläre Ausdrücke.
Das RegExp-Objekt
Zuerst finden Sie hier eine systematische Übersicht. Danach folgen entsprechende Beispiele.
Methoden
Folgende Methoden sind benutzbar:
-
exec()
: Ausführen des Tests und Rückgabe des ersten Treffers -
test()
: Ausführen des Tests und Ausgabe vontrue
oderfalse
-
toString()
: Der Ausdruck in Textform
Neben den Methoden des RegExp
-Objekts können auch viele andere Funktionen in JavaScript mit Ausdrücken umgehen, unter anderem die String
-Funktionen.
Die Methode exec
gibt ein Objekt zurück, dass folgendes enthält:
- Ein Array mit den Gruppen:
- Bei einer Gruppe steht der erste Treffer in [0]
- Bei Gruppen folgen diese in den Arrayelemente [1] .. [n]
- Die Eigenschaft
index
enthält die Position des Treffers in der Zeichenkette. Der Wert ist 0-basiert. - Die Eigenschaft
input
enthält die ursprüngliche Zeichenkette
Neben den Angaben, die als Ergebnis zurückgegeben werden, enthält auch das ursprüngliche RegExp-Objekt weitere Informationen. Dazu gehört auch die Fähigkeit, durch weitere Elemente der Trefferliste zu iterieren.
Das folgende Skript enthält einen sehr einfachen Ausdruck. Dafür führt er zu mehreren Treffern. Das sukzessive Aufrufen desselben Ausdrucks setzt intern einen Zeiger weiter, sodass die do
-Schleife alle Treffer durchläuft.
Die Ausgabe sieht nun folgendermaßen aus:
Eigenschaften
Einige Eigenschaften helfen dabei, mit den Informationen aus dem Ausdruck flexibel umgehen zu können:
-
constructor
: Funktion, die dasRegExp
-Objekt erzeugt -
global
: Prüft, ob die Option “g” gesetzt wurde (Boolean) -
ignoreCase
: Prüft, ob die Option “i” gesetzt wurde (Boolean) -
lastIndex
: Zeigt an, welchen Index der nächste Treffer hat -
multiline
: Prüft, ob die Option “m” gesetzt wurde -
source
: Der Text des Prüfmusters
Hier folgt ein Skript, da zeigt, wie der interne Zeiger weiterläuft:
Die Ausgabe sieht nun folgendermaßen aus:
Dynamische Eigenschaften
Gruppen, die in einem Ausdruck definiert werden, stehen als dynamische Eigenschaften des Objekts RegExp
zur Verfügung. Die Namen lauten $1
bis $9
, wobei jede öffnende Klammer zählt. Sie können die Zählung einer Klammer unterdrücken, indem Sie diese (?:)
schreiben (der Doppelpunkt modifiziert das Verhalten).
Die zweite Zeile gibt den Inhalt der Klammer zurück, die im Suchmuster gefunden wurde, hier also den Wert “def”.
Literalschreibweise direkt nutzen
Am einfachsten ist die Nutzung der Literal-Schreibweise:
Dieser Ausdruck gibt true
zurück. Würde das i
am Ende entfallen, entsteht false
.
Die Methoden lassen sich auch direkt ans Literal anbinden. Hier ein Beispiel für exec
:
Zurückgegeben wird nun ein Objekt mit dem Treffer “Web” an Position 0, der Trefferstelle in der Eigenschaft index
, hier der Wert 15 und dem Suchtext in der Eigenschaft input
. Wird nichts gefunden, gibt die Methode exec
den Wert null
zurück.
Wollen Sie das Objekt direkt anlegen, sieht das folgendermaßen aus:
Verarbeitungsoptionen
Einige Optionen werden nicht als Teil des Ausdrucks, sondern als Option der ausführenden Methode übergeben. In JavaScript stehen diese Optionen am Ende des Ausdrucks nach dem schließenden /
-Zeichen:
Unterstützt werden folgende Optionen:
-
i
: Groß- und Kleinschreibung wird nicht berücksichtigt -
g
: Der Ausdruck wird vollständig (global) durchsucht, auch wenn bereits eine Fundstelle erkannt wurde -
m
: Der Ausdruck durchsucht mehrzeilig (sonst wird am Ende der Zeile gestoppt)
Zeichenkettenfunktionen
Es folgen einige Beispiele, die die Syntax in JavaScript näher erläutern. Sie zeigen, dass reguläre Ausdrücke auch in anderen Funktionen einsetzbar sind.
Übersicht
Reguläre Ausdrücke lassen sich indirekt über Zeichenkettenfunktionen nutzen. Zur Verfügung stehen folgende Funktionen:
search
replace
match
split
Die Variable res enthält den Wert 15. Es wird also der Index des Treffers angezeigt. Wird kein Treffer gefunden, wird -1 zurückgegeben.
Wird statt des Index nur ein boolesches Ergebnis benötigt, eignet sich match
:
Der Ausdruck ist erfüllt und es wird Treffer ausgegeben.
Analog funktioniert replace
:
Die Variable res enthält den Wert Besuche unsere .NET-Kurse.
8.8 Zusammenfassung
Hier finden Sie die Metazeichen und Symbole auf einen Blick.
Abkürzung | Beschreibung |
---|---|
. | Ein beliebiges Zeichen |
[] | Ein Zeichen aus einem Zeichenvorrat |
[^] | Ein Zeichen, das nicht aus dem Zeichenvorrat stammt |
. | Der Punkt |
\ | Maskierung von Sonderzeichen |
\\ | Der Backslash |
Abkürzung | Beschreibung |
---|---|
() | Zählende Gruppe |
(?:) | Nicht zählende Gruppen |
(?=) | Vorausschauende Überstimmung. Passt, wenn nächstes |
Zeichen passt | |
(?!) | Vorausschauende Überstimmung. Passt, wenn nächstes |
Zeichen nicht passt |
Operator | Bedeutung | Beschreibung |
---|---|---|
? | 0 – 1 | Kein oder ein Zeichen |
* | 0 – ∞ | Kein oder beliebig viele Zeichen |
+ | 1 – ∞ | Mindestens ein oder beliebig viele Zeichen |
Zahl | Genau “Zahl” Zeichen | |
Min – ∞ | Mindestens “Min” Zeichen | |
0 – Max | Kein oder maximal “Max” Zeichen | |
Min – Max | Minimal “Min” bis maximal “Max” Zeichen | |
^ | Start; bei der Option “m” der Anfang der Zeile | |
$ | Ende; bei der Option “m” das Ende der Zeile | |
| | Logisches Oder |
Abkürzung | Beschreibung |
---|---|
\t | Tabulatorzeichen |
\n | Newline (Neue Zeile) |
\r | Return (Wagenrücklauf) |
\f | Formfeed (Seitenvorschub) |
\v | Vertikaler Tabulator |
\s | White-Space (eines der im Druck nicht sichtbaren |
Zeichen, also \t, Leerzeichen, \n, \r, \f) | |
S | Negation von \s |
\w | Wortzeichen (Zeichen, aus denen Wörter bestehen, |
konkret [_a-zA-Z0-9] | |
W | Die Negation von \w |
\d | Ziffer (engl. digit), entspricht [0-9] |
D | Negation von \d |
\b | Wortgrenze, als Anfang oder Ende eines Wortes |
zählen alle Zeichen, die nicht zur Abkürzung \w gehören. | |
B | Negation der Anweisung \b |
\0 | Null-Zeichen (physische 0) |
\xxx | Zeichenwert, dargestellt durch eine oktale Zahl |
\xdd | Zeichenwert in der hexadezimalen Form |
\uxxxx | Unicode-Zeichen in hexadezimaler Schreibweise |
\cxxx | Steuerzeichen, ASCII-Wert |
Name | Beschreibung |
---|---|
exec | RegExp-Methode, untersucht und gibt Array zurück |
test | RegExp-Methode, untersucht und gibt Boolean zurück |
match | String-Methode, Array oder null
|
search | String-Methode, Index des Treffers oder -1 |
replace | String-Methode, ersetzte Zeichenkette oder unverändert |
split | String-Methode, Array |
//o | Literal des RegExp-Objekts (o=Option, siehe nächste Tabelle) |
Abkürzung | Beschreibung |
---|---|
g | Global, auch nach Treffer weiter suchen |
m | Mehrzeilig, behandelt Zeilenumbrüche als reguläre Zeichen |
i | Groß- und Kleinschreibung nicht berücksichtigen |
9 Reflektion
Hiermit lassen sich Objekte untersuchen – also deren konkrete Eigenschaften zur Laufzeit erkunden. Viele typisierte (Compiler)-Sprachen haben eine solche Umgebung, mit der sich Code quasi selbst untersuchen kann. In Java oder C# ist dies notwendig, weil die Sprachen statisch sind und Informationen über den Code damit unveränderlich. JavaScript ist eine dynamische Sprache, was die Anwendung derartiger Techniken in vielen Fällen obsolet macht. Aber es gibt Fälle, in denen diese Technik – Reflektion – bislang fehlte.
9.1 Die Reflektions-API
ES5 hatte bereits einige wenige Hilfsfunktionen, wie Array.isArray
, Object.getOwnPropertyDescriptor
und Object.keys
. Allerdings bilden diese nur einen Teil der Funktionen ab, die technisch möglich sind. Darüber hinaus ist die API mehr oder weniger willkürlich den statischen Objektklassen zugeordnet, mal mehr mal weniger gut.
In der neuen Version ES6 wurde deshalb eine vollständig neue API mit dem statischen Objekt Reflect
eingeführt.
Reflect
Reflect
ist statisch und bietet allgemeine Funktionen zum Untersuchen von Sprachelementen auf Sprachmerkmale:
Das Beispiel zeigt die Abfrage von Eigenschaften und die Fähigkeit, einen Konstruktoraufruf indirekt auszuführen. Das letzte ist sinnvoll, um Aufrufe mit dynamischen Parametern zu versorgen.
Die API enthält einige Bereiche, die eine nähere Betrachtung sinnvoll erscheinen lassen.
9.2 Erzeugerfunktionen
Das Erzeugen von Elementen ist Teil der Dynamik. Mangels alternativer Syntax mit Schlüsselwörtern in ES5 können Eigenschaften mit defineProperty
erzeugt werden. Erst in ES6 kamen die native Schlüsselwörter get
und set
hinzu, die jedoch nicht alle funktionalen Details abbilden können.
Die Nutzung ist einfach, birgt aber ein paar Fallen:
So gibt der Aufruf das erste Argument wieder zurück (das Objekt, auf dem die Eigenschaft erzeugt werden soll). Das ist für ein fließendes Programmiermodell vielleicht nicht verkehrt, bei einer eher nicht so oft benutzten Funktion aber nicht wirklich sinnvoll.
Mit Reflect.defineProperty
wurde eine bessere Alternative geschaffen.
Der Rückgabewert ist jetzt boolean
und gibt direkt den Erfolg oder Misserfolg der Aktion an.
Operatoren für Schlüsselwörter
Schlüsselwörter dynamisch zu benutzen ist eine der großen Herausforderungen für eine Sprache. So lässt sich in ES5 eine Eigenschaft mit dem Schlüsselwort delete
wieder entfernen. Wenn aber dieser Vorgang in Abhängigkeit von anderen Bedingungen ausgeführt werden soll, dann wird des kompliziert, weil der Code in eine Funktion abstrahiert werden muss.
Ab ES2015 gibt es nun die Methode Reflect.deleteProperty
.
Dynamische Argumentelisten
Besonders bei Konstruktoraufrufen sind dynamische Argumentlisten schwer zu verarbeiten. Intern muss immer auf Hilfsfunktionen wie apply
zurückgegriffen werden. Damit das halbwegs nutzbar wird, ist eine Wrapper-Funktion sinnvoll. Der Code wird dadurch unnötig komplex.
Besser geht es mit dem Spread-Operator:
Das Objekt Reflect
bietet auch dafür eine Alternative:
9.3 Funktionsaufrufe
Dynamische Argumentlisten lassen sich, wie bereits erwähnt, mit apply
als Array übergeben (oder call
für eine direkte Auflistung). Zusätzlich kann der Kontext this
als erstes Argument übergeben werden:
Das Risiko besteht darin, dass ein sinnfreier, fremder Kontext benutzt wird. JavaScript bietet hier keinen Schutz.
Etwas Schutz bietet eine weitere Überladungsform:
All das beachtet der Spread-Operator in ES6:
Nun ist aber die Möglichkeit, this
zu manipulieren, nicht mehr gegeben. An dieser Stelle greift nun eine weitere Methode des Reflect
-Objekts:
9.4 Proxy-Fallen
Lesen Sie das folgende Kapitel über Proxies, wenn Sie dieses Konzept noch nicht kennen. Proxies werden in Aufrufe implementiert, um den Datenfluss zu überprüfen. Damit lassen sich globale Aktionen elegant automatisieren. Das folgende Beispiel zeigt dies:
In diesem Beispiel wird der Aufruf der Eigenschaft a, die eigentlich nur “b” zurückgibt, gekapselt. Der Proxy fängt den Aufruf ab und leitet ihn auf die Untersuchungsfunktion handler um. Da es sich um eine Eigenschaft handelt, wird hier get benutzt. Damit das ursprüngliche Verhalten weiter benutzt werden kann, wird Reflect.get
benutzt (Zeile 3), um den Aufruf auszuführen. Dieser Teil könnte syntaktisch noch verkürzt werden:
Diese Technik, Codeflüsse zu untersuchen, wird als Falle (trap) bezeichnet. Die generelle Syntax lautet:
Zusammenfassung der Reflect-Methoden
Hier finden Sie alle Reflect
-Methoden in der Übersicht:
-
Reflect.apply()
: Aufruf einer Funktion mit Argumenten -
Reflect.construct()
: Aufruf des Konstruktors mit Argumenten -
Reflect.defineProperty()
: Erzeugen einer Eigenschaft -
Reflect.deleteProperty()
: Entfernen einer Eigenschaft -
Reflect.get()
: Aufruf (lesen) einer Eigenschaft -
Reflect.set()
: Aufruf (schreiben) einer Eigenschaft -
Reflect.getOwnPropertyDescriptor()
: Abruf des Beschreibungsobjekts einer Eigenschaft -
Reflect.getPrototypeOf()
: Abruf des Prototypen -
Reflect.has()
: Prüft, ob ein Mitglied existiert -
Reflect.isExtensible()
: Prüft, ob ein Objekt um weitere Mitglieder erweiterbar ist -
Reflect.ownKeys()
: Ruft alle Eigenschaftsnamen (Objektschlüssel) als Array ab -
Reflect.preventExtensions()
: Schränkt die Erweiterbarkeit ein -
Reflect.setPrototypeOf()
: Setzt den Prototypen
Eine Fortsetzung der Beschreibung finden Sie auf MDN.
Ein Hinweis noch zu dem Begriff keys. Objekte in JavaScript sind Objekt-Verzeichnisse (maps). Die Namen der Mitglieder eines Objekts werden wie die Schlüsselliste eines Verzeichnisses (ein Objekt mit Schlüssel/Werte-Paaren) behandelt. Ein key kann also durchaus der Name einer Eigenschaft oder auch einer Methode sein. Deshalb werden diese Information mit ownKeys
abgerufen.
10 Dekoratoren
Dekoratoren bieten eine weitere Beschreibung von Objekten, die bei der Nutzung lesbar und im aktuellen Code nicht direkt zugreifbar ist. So haben Sie eine weitere Dimension zur Verfügung, die vor allem das Grundprinzip Separation of Concerns erfüllt. Dabei geht es darum, logisch nicht zusammenhängende Dinge auch codeseitig zu trennen.
10.1 Annotationen
Die in ECMAScript benutzten Dekoratoren sind konkrete Ausprägungen eines Code-Prinzips, das auch als Annotationen bezeichnet wird. Dabei werden aktivem Code Beschreibungsinformationen eingepflanzt, die nicht den Code-Fluss betreffen, dennoch aus laufendem Code heraus abfragbar sind.
Die Dekoratoren
Im Quellcode von TypeScript ist die Deklaration der Dekoratoren zu finden. Sie unterscheiden sich vor allem im Ort, wo sie benutzt werden dürfen. Die Typ-Definition hilft zu verstehen, wie die Dekoratoren in reinem JavaScript funktionieren.
Es gibt also vier Platzierungsmöglichkeiten:
- Methoden
- Klassen bzw. Funktionen
- Eigenschaften
- Parameter
Schreibweisen
Schaut man auf die Beispiele in der Dokumentation und die Benutzung bei beispielweise Angular, so fällt ein wesentlicher Unterschied auf. Manche Dekoratoren haben Klammern (wie Funktionsaufrufe), manche nicht. Der Grund ist die Art, wie der Dekorator definiert wird.
- @log: Direkter Aufruf einer Dekorator-Funktion
- @Log(): Aufruf einer Dekorator-Factory mit indirektem Aufruf des Dekorators
Die Factory-Version hat den Vorteil, das Parameter übergeben werden können. Der Aufwand zum Erstellen ist dafür etwas höher. Die folgenden Beispiele zeigen eine einfache Version ohne Factory. Das Praxis-Beispiel danach, das aus einem Angular-Projekt stammt, nutzt Factory-Funktionen.
10.2 Dekoratoren für Methoden
Aus der Deklaration lässt sich die Signatur entnehmen. Mit dieser Information kann ein erster Dekorator selbst implementiert werden. Für eine Methode sieht dies dann beispielsweise folgendermaßen aus:
Dieser Dekorator protokolliert den Methodenzugriff durch eine Ausgabe auf der Konsole. Ausgegeben werden der Funktionsname, die Argumente und der Rückgabewert der Methode. Die Nutzung des Konstrukts erfolgt nun, indem der Methode der Name des Dekorators, angeführt durch ein ‘@’-Zeichen, vorangestellt wird (Zeile 2):
Die Signatur hat drei Argumente. Der erste ist die Methode selbst, die dekoriert wird. Der zweite ist der Name derselben Methode. Der Vorteil der Übertragung der Methode und des Namens besteht darin, dass die Methode im Dekorator selbst aufgerufen (ausgeführt) werden kann. Das dritte Argument ist ein Beschreibungsobjekt vom Typ TypedPropertyDescriptor
, wenn ein solcher existiert. Ist er nicht vorhanden, dann ist der Wert undefined
. Der TypedPropertyDescriptor
wird beschafft, indem Object.getOwnPropertyDescriptor()
intern aufgerufen wird. Die Übertragung der Parameter selbst und der Aufruf des Dekorators erfolgt im Code implizit.
Arbeitsweise der Polyfills
Sollte ein Polyfill benutzt werden, weil die gewählte Umgebung Dekoratoren nicht unterstützt, wird das ausgelieferte JavaScript etwa folgendermaßen aussehen:
Der Dekorator ist also nichts weiter, als eine dynamisch hinzugefügte Eigenschaft:
Der Kern der Funktion basiert also auf defineProperty
. Allerdings benutzt die neu erstellte Eigenschaft einen Methodenaufruf mit dem Namen __decorate. Es handelt sich also lediglich um eine Art Wrapper zum Aufruf einer andernorts deklarierten Funktion, die die eigentliche Arbeit erledigt. Es ist naheliegend, dass der TypeScript-Compiler hier den nötigen Code mitbringt und zentral bereitstellt (während der gezeigte Aufruf spezifisch für die Methode ist, wird die _decorate-Methode nur einmal erzeugt. Dies sieht folgendermaßen aus (aufbereitet zugunsten der Lesbarkeit):
Der ||-Operator in Zeile 1 verhindert, dass die Deklaration mehrfach erfolgt. Dann wird in Zeile 2 geprüft, ob es sich beim Aufrufziel um eine Funktion handelt. Die Syntax mag seltsam anmuten, aber es handelt sich hier um einen Einsatz der Metadaten-Reflektions-API in JavaScript. Diese wird später in diesem Kapitel noch erläutert. Falls die unterliegende JavaScript-Implementierung dies noch nicht unterstützt, ist Reflect undefined
. Dann wird die Bedingung nicht erfüllt und der Code fällt auf den Rückfallteil mit der switch
-Anweisung durch.
Der switch
-Parameter argument.length ist nun die Anzahl der Parameter, __decorate übergeben wurden. Dies sah folgendermaßen aus:
In diesem Beispiel wurden vier Parameter übergeben. Es wird also der letzte Teil ausgeführt. Die eigentliche Arbeit erledigt reduceRight
hier. Diese Methode führt eine Funktion für jedes Element eines Arrays aus, beginnend von rechts, und gibt dann einen skalaren Wert zurück. Die Namen o und d sind hier nicht hilfreich, aber durch die Platzierung kann man sich die Bedeutung erschließen. Es handelt sich hier um die Funktion selbst und den Funktionsnamen der dekorierten Methode. Ohne den mehrfach indirekten Aufruf würde der Dekorator auf direktem Wege folgendermaßen aussehen:
Dieser Aufruf der log-Funktion zeigt, wie die Parameter schlussendlich übergeben werden und wo sie herkommen. Er zeigt auch, warum log überhaupt aufgerufen wird.
Nun soll noch die log-Funktion selbst untersucht werden:
Die Argumente werden folgendermaßen befüllt:
- target === Example.prototype
- key === “oneFunction”
- value === Object.getOwnPropertyDescriptor(Beispiel.prototype, “oneFunction”)
In Zeile 4 werden die Argumente in eine Zeichenkette konvertiert. In Zeile 5 wird die Methode selbst aufgerufen und der Rückgabewert ermittelt. Warum das so aussieht ergibt die Dokumentation von PropertyDescriptor
. In Zeile 6 wird das Ergebnis in eine Zeichenkette verwandelt. Zeile 7 gibt es aus. Zeile 8 gibt das Ergebnis des Aufrufs zurück, damit der ursprüngliche Aufruf wie zuvor (ohne Dekorator) funktioniert.
10.3 Dekoratoren für Eigenschaften
Die Signatur für den Dekorator wurde bereits am Anfang gezeigt:
Die Platzierung erfolgt nun auf einer Eigenschaft:
Erneut handelt es sich um eine simple Protokollfunktion. Der Aufruf ist ähnlich wie für die Methode, allerdings fehlt der letzte Parameter beim Aufruf von __decorate.
Auch wird hier nicht mehr der Rückgabewert benutzt, es fehlt das return
-Schlüsselwort.
Hier nun die eigentliche Implementierung des Dekorators:
In Zeile 2 wird der Wert der Eigenschaft ermittelt. In Zeile 3 und Zeile 7 werden die Zugriffpfade auf die Eigenschaft benutzt, um den Zugriff aufzufangen und Konsolenausgaben zu erzeugen. Der permanente Zugriff auf _val gelingt, weil es sich hier um einen Funktionsabschluss (Closure) handelt. Das Löschen mittels delete
in Zeile 11 entfernt die ursprüngliche Eigenschaft und ersetzt sie dann durch das eigene Konstrukt. Die Ausgabe sieht folgendermaßen aus (für das vollständige Skript mit Demo-Daten):
10.4 Dekoratoren für Klassen
Die Deklaration soll hier wieder als Ausgangspunkt dienen:
Der Einsatz erfolgt nun auf der Klasse selbst:
Übergeben wird hier der die Konstruktor-Funktion selbst, nicht der Prototyp. Aus dieser Angabe lassen sich alle nötigen Informationen gewinnen, die Angabe einer Eigenschaftenbeschreibung ist nicht notwendig. Der Konstruktor selbst wird durch den Aufruf von __decorate überschrieben (Zeile 6). Wird die Klasse später benutzt, wird der Konstruktor aufgerufen, damit wird implizit erst der Dekorator ausgeführt. Dekorierte Klassen führen den Dekorator also am Anfang der Instanziierung einmalig aus. Mit diesem Wissen lässt sich die Implementierung des Dekorators selbst leicht vornehmen.
Der übergebene Parameter target ist der Konstruktor der Klasse, die dekoriert wird. Die Funktion construct ist eine Hilfsfunktion, die aufgerufen wird, wenn die dekorierte Klasse instanziiert wird. Das erste Argument ist der Konstruktor selbst, der Rest die Liste der Argumente des Konstruktors der dekorierten Klasse. Die Variable c in der Hilfsfunktion enthält den gekapselten Konstruktoraufruf und dieser wird mit new c()
hier ausgeführt. Die vererbaren Mitglieder der Funktion werden über den Prototyp übergeben.
Das folgende Beispiel erzeugt zwei Konsolenausgaben, einmal über den Dekorator und einmal als Beweis, dass das Konstruktorobjekt noch wie zuvor Person ist und nicht der Dekorator, obwohl der Konstruktor überschrieben wurde.
Dekorator für Parameter
Auch Parameter lassen sich dekorieren. Erneut soll die Signatur als Ausgangspunkt dienen:
Der Einsatz auf der bereits mehrfach benutzten Klasse Person sieht folgendermaßen aus (Zeile 11):
Im folgende Beispiel ist der Index des Parameters in Zeile 2 gleich 0, in Zeile 5 dagegen ist es 1:
Der Dekorator für Parameter braucht also selbst drei Parameter:
- Prototyp der dekorierten Klasse
- Name der Methode, deren Parameter dekoriert ist
- Position in der Liste der Parameter dieser Methode (der Index)
Ein mögliche Implementierung sieht nun folgendermaßen aus:
Hier wird der Klasse, die die Methode mit dem dekorierten Parameter enthält (target), eine Eigenschaft metadataKey hinzugefügt. Es handelt sich also um einen Speicher für Metadaten. Die Metadaten bestehen aus den Indizes der Parameter. Das ist auch der Sinn der Parameter-Dekoratoren – Erstellen von Metadateninformationen. Die Auswertung findet dann in anderen Dekoratoren statt. Im folgende Beispiel wird der Dekorator logParameter (Zeile 12) benutzt, um dem Dekorator für die Methode @logMethod (Zeile 11) mitzuteilen, dass nur dieser Parameter analysiert werden soll.
Der dazu angepasste Dekorator der Methode sieht nun folgendermaßen aus:
Fabrikfunktionen für Dekoratoren
Fabrikfunktionen für Dekoratoren sind in der offiziellen TypeScript-Beschreibung folgendermaßen definiert:
Erneut soll die Klasse Person als Ausgangspunkt dienen:
Das funktioniert, aber es ist doch etwas umständlich, immer wieder neue Dekoratorklassen zu bauen, für alle Arten von Anwendungen. Es wäre deutlich eleganter, die Zugriffsprotokollierung hier folgendermaßen vornehmen zu können:
Die Fabrikfunktion ist hier logAll, die ihrerseits eine Aufspaltung auf die einzelnen Dekoratoren vornimmt.
Genau diesen Zweck erfüllen Fabrikfunktionen für Dekoratoren. In der Praxis sieht das folgendermaßen aus:
Als Unterscheidungsmerkmal dient schlicht die Anzahl der Parameter, die bei jedem Dekoratortyp unterschiedlich ist. Einzig bei der Eigenschaft ist hier die Zuordnung uneindeutig, was an einem Bug im Transpiler liegt (der Fehler tritt erst im JavaScript auf). Beobachten Sie das Verhalten und passen Sie den Code gegebenenfalls an.
Konfigurierbare Dekoratoren
Argumente für Dekoratoren verschaffen diesen eine weiteren Einsatzbereich. Dies ist möglich, wenn Fabrikfunktionen mit Parameteroptionen für die Definition der Dekoratoren benutzt werden. Die Nutzung sieht dann folgendermaßen aus und unterscheidet sich etwas vom vorhergehenden Beispiel:
Hier soll eine Fabrikfunktion als Muster für die Implementierung dienen:
Der hier als Parameter comment übergebene Wert ist der Parameter des Dekorators. Als Typ wurde hier string
angenommen, aber dies ist willkürlich und beliebig. Oft sind komplexere Objekte ein guter Weg, viele Parameter zusammenzufassen und damit lesbare Signaturen zu erzeugen.
11 Stellvertreter: Proxies
Proxies erlauben das Erstellen von Objekten. Der Eingriff in den Erstellungsprozess erlaubt weitreichende Einsatzmöglichkeiten. Dazu gehört neben direktem Code-Zugriff auf das Loggen oder das Erstellen virtueller Objekte. Proxies erlauben es, Zugriffe auf Eigenschaften zu kontrollieren. Es handelt sich um Eingriffe in den Programmfluss. Proxies sind damit Ereignissen relativ ähnlich. Mögliche Anwendungen sind Protokollierung, Auditing, globale Ereignisbehandlungsmethoden oder Testfunktionen.
11.1 Einführung
Bei einem einfachen Objekt sieht das folgendermaßen aus:
Hier wird ganz allgemein in den Get-Zweig einer Eigenschaft eingegriffen – jeder Eigenschaft. Die Eigenschaft Welt im Beispiel ist willkürlich gewählt. Die Ausgabe zeigt folgendes an:
Bei Funktionen sieht es ähnlich aus. Statt get
wird hier apply
benutzt:
Hier erfolgt erst die Ausgabe “Interceptor” und dann der Aufruf der ursprünglichen Funktion target. Der Aufruf kann aber auch unterdrückt werden.
Das folgende Listing 10.4 zeigt eine vereinfachte Vorgehensweise:
Der Proxy wird um das ursprüngliche Objekt, hier target herum gelegt und bildet den Zugriff nun ab. Spannend wird es, wenn nun der Proxy-Aufruf weitere Aktionen enthält und dann den ursprünglichen Aufruf ausführt.
11.2 Proxy-Fallen anwenden
Eingriffe werden auch als Fallen (traps) bezeichnet. Dies erfolgt meist auf den Eigenschaftszugriffen, also get
oder set
.
Der Getter (get)
Das folgende Beispiel zeigt die Protokollierung von lesenden Zugriffen auf eine Eigenschaft.
Der Setter (set)
Auch beim Schreiben gibt es interessante Möglichkeiten. So kann der Zugriff auf private Mitglieder, die oft nur durch den Präfix “_” gekennzeichnet werden, leicht unterdrückt werden. Auch die Auswertung der speziellen Namen in Angular, die per Konvention mit “$” oder “$$” beginnen, nutzt eine ähnliche Technik.
Die Ausgabe in Abbildung 10.1 zeigt, wie das Skript reagiert.
Das ganze Konstrukt lässt sich noch in eine Hilfsfunktion makeProxy verpacken und dann bequem nutzen:
Der Einsatz unterscheidet sich nicht vom vorhergehenden Beispiel. Der Proxy sorgt für echte private Eigenschaften und damit werden Programmierfehler vermieden, ohne dass der Aufwand zur Zugangsprüfung exorbitant steigt. Nun kann das im Kapitel zu objektorientierten Programmierung benutzte Entwurfsmuster auch benutzt werden, um Eigenschaften zu verstecken. Das setzt aber voraus, dass man beim Entwurf der Objekte gleich daran denkt, diese clever zu bauen. Wenn nun eine Bibliothek bereits lange existiert und in einem neuen Projekt eingesetzt werden soll, dann ist es etwas schwierig, bestehenden Code zu ändern. Zumindest liegt hier ein erhebliches Risiko und hoher Aufwand. Proxy-Konstrukte sind dagegen elegant nachträglich benutzbar, weil sie sich um bestehenden Code herumlegen.
11.3 Schemaprüfung mit Proxies
Schemaprüfungen können jederzeit innerhalb des Objekts durchgeführt werden. Eleganter ist freilich die Trennung. Dies kann in auch mit Dekoratoren erfolgen. Ist die Benutzbarkeit nicht sicher, können an deren Stelle Proxies benutzt werden.
Die Datenstruktur des zu prüfenden Objekts ist POJO (Plain Old JavaScript Object). Weder Klassen noch prototypische Vererbung kommen hier zum Einsatz. Ein solches einfaches Objekt ist der Typ person im folgenden Beispiel.
Die Prüffunktion schaltet sich zwischen Aufrufer und Daten. Lediglich der Datenzugriff (set
) wird geprüft. Erfolgt ein Verstoß gegen die Regel, dann wird eine Ausnahme vom Type TypeError
erzeugt. Der Vorteil liegt in der Konstruktion des Objekts person in Zeile 13, das signifikant einfacher ist und eher dem klassischen Stil in JavaScript entspricht. Eine vergleichbare, dekorierte Klasse würde dagegen folgendermaßen aussehen:
Den Prüfcode selbst muss man freilich außerdem noch hinterlegen. Insgesamt ganz gut lesbar und verständlich, aber auch aufwändig. Proxies sind ein guter Kompromiss und fügen sich fein in die JavaScript-Kultur ein.
11.4 Entfernbare Proxies
Proxies lassen sich mit revoke()
praktisch stilllegen. Dazu wird ein Objekt mit der Struktur { proxy, revoke }
zurückgegeben. Der Aufruf ist nur einmal notwendig, nachfolgende Aufrufe werden ignoriert.
Damit lässt sich der Zugriff auf den Proxy kontrollieren. Ist dies nicht mehr länger gewünscht, wird das Objekt zerstört. Der Zugriff ist nicht mehr möglich. Der Anwendungscode kann dies feststellen und dann entsprechend reagieren. Theoretisch ist es möglich, dass der Proxy sich nach dem ersten erfolgreichen Zugriff selbst entfernt und damit eine Art Einmal-Proxy ist. Einen praktischen Nutzen hat dies möglicherweise nicht.
11.5 Proxy-API
Und hier eine vollständige Liste aller Eingriffsmöglichkeiten:
-
get
: target.prop -
set
: target.prop = value -
has
: ‘prop’ in target -
deleteProperty
: delete target.prop -
apply
: target(…args) -
construct
: new target(…args) -
getOwnPropertyDescriptor
: Object.getOwnPropertyDescriptor(target, ‘prop’) -
defineProperty
: Object.defineProperty(target, ‘prop’, descriptor) -
getPrototypeOf
:
-
setPrototypeOf
:
-
enumerate
: for (let i in target) {} -
ownKeys
: Object.keys(target) -
preventExtensions
: Object.preventExtensions(target) -
isExtensible
: Object.isExtensible(target)
Tipps und Tricks
In diesem Abschnitt werden einige Eigenschaften behandelt, die für die professionelle Programmierung sinnvoll sind.
Strikt-Mode use strict
Eine Funktion oder Skript kann in den Strikt-Mode versetzt werden. Ist das der Fall, wird der JavaScript-Interpreter etwas strenger:
* Gültigkeitsbereich (scope): Variablen müssen statisch gebunden sein (verhindert with
und eval
).
* Implizite globale Variablen werden unterbunden (windows.myGlobal
)
* Verhindert “global leakage”: Das Schlüsselwort this
wird nicht an window
sondern als undefined
gebunden.
* Provoziert “noisy failure”: beispielsweise wird die Wertzuweisung an eine ReadOnly-Eigenschaft eine Ausnahme auslösen.
* Bei Oktalzahlen wird eine vorangestellte 0 entwertet und verhindert, dass 0100 = 64 wird.
Allgemeine Tipps
* Vergessen Sie nie var
(oder let
)
* Benutzen Sie immer === statt ==
* Beenden Sie Zeilen mit dem Semikolon ;
* Verwechseln Sie nicht instanceof
und typeof
* Kapseln Sie privaten Code in selbstaufrufende Funktionen (function(){ ... })()
Spezielle Tipps
Dies ist eine Sammlung von Tricks für praktische Anwendungen.
Zufallswert aus einem Array holen
Zufallszahl aus einem Bereich
Array mit fortlaufenden Zahlen
Set mit Zeichen erzeugen
Zahlenarray durcheinander bringen
Leerzeichen entfernen (trim)
Neuere JavaScript-Umgebungen haben
trim
bereits eingebaut.
Arrays kombinieren
Argumente in Array konvertieren
Wert auf Zahl testen
Wert auf Array testen
Neuere Browser kennen Array.isArray(obj);
und benötigen dies nicht mehr.
Minimum und Maximum in einem Array
Array leeren
Array-Element entfernen
Folgendes geht nicht, da delete
das Element auf undefined
setzt, aber nicht entfernt.
Folgendes funktioniert:
Array abschneiden
Runden
Gleitkommetipps
Warum passiert das? 0.1 +0.2 ist intern 0.30000000000000004. Das passiert, wenn rationale Zahlen in einem 64bit-Raum abgebildet werden.
Mit toFixed()
und toPrecision()
kann dies gelöst werden.
Zugriff auf Prototyp vermeiden
Dies ist vor allem sinnvoll, wenn das Basisobjekt sehr groß ist, beispielsweise ist dies bei HTML-Elementen der Fall.
Der Komma-Operator
Prüfe bevor isFinite()
benutzt wird
Vermeide negative Indizes in Arrays
for in ist nicht gut in Arrays
Folgendes ist keine gute Idee:
So sollte es aussehen:
Folgendes ist auch keine so gute Idee:
Dabei wird die Länge bei jedem Durchlauf neu berechnet. Sie ändert sich aber möglicherweise nie. Neuere Engines berücksichtigen dies automatisch.
Anmerkungen
Objektorientierung
1Das ist unklar übersetzt in der Literatur. In objektorientierten Sprachen nennt man das üblicherweise “overwrite” (überschreiben), während bei JavaScript der Begriff “override” benutzt wird, der dann mit “überdecken” übersetzt wird, weil die Übersetzungen nach Wörterbuch alle nicht so recht passen.↩
Funktionen
1Im Deutschen auch als “ergibt sich zu” bezeichnet, oder umgangsprachlich “Pfeilfunktion”.↩