Einführung in JavaScript
Einführung in JavaScript
Jörg Krause
Buy on Leanpub

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:

1 function test(d){
2   return (d != undefined);
3 }

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):

1 (function (window,
2            document,
3            undefined) {
4 
5     /* ... */
6 
7 })(window, document);

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:

Listing 1.1: Hallo JavaScript (hello.html)
 1 <html>
 2 <head>
 3 <title>Hallo JavaScript</title>
 4 <script type="text/javascript">
 5 function sayHelloTo(name) {
 6   document.write("Hallo " + name + " !");
 7 }
 8 </script>
 9 </head>
10 <body>
11 
12 <script type="text/javascript">
13 document.write("Hallo  JavaScript !\n");
14 sayHelloTo("Joerg");
15 </script>
16 
17 </body>
18 </html>

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:

1 /*
2 Ein mehrzeiliger Kommentarblock
3 Kann mit den aus C/C++/C#/Java bekannten Blockkommentarzeichen
4 erstellt werden.
5 */

Der Zeilenkommentar steht alleine auf einer Zeile oder am Ende:

1 // Dies tut das:
2 var i = 42; // Zuweisung

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 (\\).

Tabelle 1.1: Sonderzeichen
Zeichen Bedeutung
\b BackSpace
\n NewLine
\t Tab
\f FormFeed
\r CarriageReturn

Ein Beispiel dazu:

'Das ist eine mehrzeilige\r\nZeichenkette'

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:

alert("über"));

Schreiben Sie jedoch folgendes, wenn Umlaute benutzt werden und die Fähigkeiten der Zielumgebung unklar sind:

alert(unescape("%FCber"));

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.

var bin = 0b111110111;
var oct = 0o767;

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.

Tabelle 1.2: Wichtige Mathematische Operatoren
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:

if (NaN == NaN) {
  // Dieser Teil wird niemals erreicht
}

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:

var n = Number(value);

Dies konvertiert einen Wert in eine Zahl. Im Fehlerfall entsteht NaN. Konvertiert werden Zahlen besser durch Operatoren. Mit + sieht das dann folgendermaßen aus:

var z = "23";
var n = +z;

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.

var a = isNaN(undefined);        // true (undefined nicht sinnvol\
l)
var b = Number.isNaN(undefined); // false (undefined == 0, ist ok)

Die Funktion parseInt(value, radix) verwandelt in eine Ganzzahl mit einer definierten Zahlenbasis (bei Dezimalzahlen ist die Basis 10). Im Fehlerfall entsteht wieder NaN:

var i = parseInt("1F", 16); // entspricht 31

Falls eine Gleitkommazahl als Eingabe erwartet wird, eignet sich parseFloat(value)`, hier natürlich ohne Zahlenbasis:

var f = parseFloat("100.42"); // entspricht 100.42

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:

0.1 + 0.2 == 0.3; // false

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.

Abbildung 1.1: Herleitung dezimaler Zahlen aus binären

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.

console.log(9007199254740993); // 9007199254740992

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.

1 Number.isSafeInteger(number)
2 Number.MIN_SAFE_INTEGER
3 Number.MAX_SAFE_INTEGER

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):

const x = Math.<Eigenschaft>;
const x = Math.<Methode(<Parameter>)>;

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

1 function toDegrees (angle) {
2   return angle * (180 / Math.PI);
3 }
4 
5 function toRadians (angle) {
6   return angle * (Math.PI / 180);
7 }

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.

Abbildung 1.2: Kreisbahn mit Sinus und Kosinus

Die folgende Funktion (Listing 1.2) zeichnet einen Viertelkreis:

Listing 1.2: Erzeugen eines Viertelkreises (qcircle.js)
 1 var canvas = document.getElementById('canvas')
 2 var ctx = canvas.getContext("2d");
 3 
 4 var step = 2 * Math.PI / 40; // Schritte
 5 var h = 150;
 6 var k = 150;
 7 var r = 50;
 8 
 9 ctx.beginPath();
10 
11 for (var theta = 0; theta < 2 * Math.PI; theta += step) {
12   var x = h + r * Math.cos(theta);
13   var y = k - r * Math.sin(theta);
14   ctx.lineTo(x, y);
15 }
16 
17 ctx.closePath();
18 ctx.stroke();

Das dazu passende HTML-Element sieht folgendermaßen aus:

<canvas id="canvas" width="150" height="150"></canvas>
Abbildung 1.3: Gezeichneter Viertelkreis mit Sinus und Kosinus

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.

let pages = Math.ceil(83 / 10);

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):

Listing 1.3: Verbesserte Rundungsfunktion (round.js)
1 var roundNumber = function(number, decimals) {
2   decimals = decimals || 2;
3   return Math.round(number * Math.pow(10, decimals))
4        / Math.pow(10, decimals);
5 };

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:

Abbildung 1.4: Ausgaben der Konstanten

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.

Abbildung 1.5: Kodierung von Schriftzeichen

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 “`”.

 1 var s;
 2 // Einfache Zeichenkette
 3 s = `Dies ist eine Vorlagenzeichenfolge`;
 4 console.log(s);
 5 // Mehrzeilig ist erlaubt
 6 s = `In ES5 ist dies
 7      nicht erlaubt.`;
 8 console.log(s);
 9 // Interpolation von Variablen
10 var name = "Jörg", time = "heute";
11 s = `Hallo ${name}, wie geht es dir ${time}?`
12 
13 // Sonderzeichen sind möglich
14 s = String.raw`In ES ist "\n" ein Umbruch.`
15 console.log(s);
16 
17 s = `In ES ist "\n" ein Umbruch.`
18 console.log(s);
Abbildung 1.6: Deutsches Layout mit Backtick rechts oben

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:

1 function html(){
2   [...arguments].forEach(a => console.log(a));
3 }
4 let title = "Joerg Krause";
5 let you = "Joerg";
6 html`<p title="${title}">Hello ${you}!</p>`;

Die Ausgabe sieht dann folgendermaßen aus:

[ '<p title="', '">Hello ', '!</p>' ]
Joerg Krause
Joerg

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.
var str = "Bitte legen Sie 'Produkte' weg!";
var pos = str.indexOf("Produkte");
lastIndexOf()
Wenn mehrere Vorkommen einer Zeichenkette in einer anderen existieren, wird die die letzte Fundstelle zurückgegeben. Wird nichts gefunden, wird -1 zurückgegeben.
var str = "Bitte legen Sie 'Produkte' weg!";
var pos = str.lastIndexOf("Produkte");
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 akzeptiert search auch reguläre Ausdrücke als Argument.
var str = "Bitte legen Sie 'Produkte' weg!";
var pos = str.search(/Produkte/);

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.
var str = "Apfel, Banane, Kiwi";
var res = str.slice(7,13);
var str = "Apfel, Banane, Kiwi";
var res = str.slice(-12,-6);

Die Ausgabe ist in beiden Fällen ‘Banane’.

var str = "Apfel, Banane, Kiwi";
var res = str.substring(7,13);
var str = "Apfel, Banane, Kiwi";
var res = str.substr(7,6);

Die Ausgabe ist auch in diesen beiden Fällen ‘Banane’.

replace(search, replace)
Diese Methode ersetzt Teile einer Zeichenkette durch eine andere:
str = "Bitte benutze Node!";
var n = str.replace("Node","AngularJS");
toUpperCase
Wandelt alle Zeichen in Großbuchstaben um.
toLowerCase
Wandelt alle Zeichen in Kleinbuchstaben um.
1 var text1 = "Hallo JavaScript!";
2 var text2 = text1.toUpperCase();
3 console.log(text2);
4 var text2 = text1.toLowerCase();
5 console.log(text2);
concat()
Diese Methode kann mehrere Argumente annehmen und kombiniert diese zu einer Zeichenkette.
1 var text1 = "Hallo";
2 var text2 = "JavaScript";
3 text3 = text1.concat("	",text2);

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.
1 var str = "HELLO WORLD";
2 str.charAt(0);            // Ergibt: H
3 str.charCodeAt(0);        // Ergibt: 72

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.

1 var txt = "a,b,c,d,e";   // String
2 txt.split(",");          // An Komma teilen
3 txt.split(" ");          // An Leerzeichen teilen
4 txt.split("|");          // Am Pipe-Symbol teilen

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

1 var txt = "Hello";       // String
2 txt.split("");           // In Array umwandeln

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.

Tabelle 2.2: Browserspezifische Funktionen
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:

let html = String.prototype.sup();

Eine String-Instanz ist nicht effektiver, aber effektiver zu beschaffen:

let html = "".sup();

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:

1 var Person = {}; // dies ist erforderlich
2 Person.Vorname = "Joerg";
3 console.log(Person.Vorname);

Neben der Punktschreibweise kann noch die Index-Schreibweise benutzt werden. Auch hier handelt es sich nur um eine Eigenschaft (Zeile 2):

1 var Person = {}; // dies ist erforderlich
2 Person["Meine Spezialeigenschaft"] = "Spezial Wert";
3 console.log(Person["Meine Spezialeigenschaft"])

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

const value = nullable ?? 0;

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:

const value = objMightBeNull?.propertyMightBeNull?.Property;

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.

1 let token = Symbol('Foo');

Die Zeichenfolge ist optional und dient nur der Dokumentation. Deshalb gilt:

1 Symbol('Foo') !== Symbol('Foo')

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:

1 const MY_KEY = Symbol();
2 let obj = {
3    [MY_KEY]: 123
4 };

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:

1 const COLOR_BLUE = 'COLOR_BLUE';
2 function colorName(c) {
3   switch(c) {
4     case COLOR_BLUE:
5       return 'blue'
6     
7   }
8 }
9 let cName = colorName('COLOR_BLUE');

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.

 1 const COLOR_RED = Symbol();
 2 const COLOR_GREEN = Symbol();
 3 const COLOR_BLUE = Symbol();
 4 function colorName(c) {
 5   switch(c){
 6     case COLOR_BLUE:
 7       return 'blue'
 8     default:
 9       throw new Error('Unknown color option');
10   }
11 }
12 let cName = colorName(COLOR_BLUE);

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
1 if (this instanceof Person) {
2    // Aktion, wenn passender Objekttyp
3 }

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:

1 function myFunction(a, b, c) { }
2 var args = [10, 20, 30];
3 myFunction(...args);

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:

1 var parts = ['shoulders', 'knees'];
2 var more = ['head', ...parts, 'foot', 'toes'];

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:

1 if (a) {
2   return a.member;
3 } else {
4   return a;
5 }

Die folgende Kurzschreibweise mit guard-Operator ist weitaus einprägsamer:

return a && a.member;

Der Nullkettenoperator, der mit ES2020 eingeführt wurde, vereinfacht diesen Aufruf nochmals:

return a?.member;

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:

1 var meinString;
2 if (meinString == undefined || meinString.length == 0) {    
3    meinString = "mein wert"; 
4 } 

Die Kurzschreibweise mit default-Operator ist weitaus einprägsamer:

var meinString; 
meinString = meinString || "mein wert";

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:

var someExpression = 14;
var someBoolean = !!someExpression;

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.

1 if (Bedingung) {
2   Anweisung1;
3   Anweisung2;
4 } else {
5   Anweisung3;
6 }

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.

1 while (Bedingung) {
2   Anweisungen ;
3 }

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.

1 do {
2   Anweisungen ;
3 } while (Bedingung);

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:

1 for (Anfangswert; Bedingung; Iterationsanweisung) {
2   // Anweisungen;
3 }

Ein praktisches Beispiel zeigt das folgende Listing 1.6.

Listing 1.6: Berechnung der Quadratzahlen von 1 bis 10
1 for (i = 1; i < 11; i++) {
2   console.log("I = " + i + " I*I = " + (i*i) + "<br />");
3 }

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):

1 var ausgabe = "";
2 for (var eigenschaft in document) {
3     ausgabe = ausgabe + "document." + eigenschaft + ": " +
4        document[eigenschaft] + "<br>";
5 }
6 document.write("<h1>Eigenschaften des Objekts " +
7                "<i>document</i></h1>");
8 document.write(ausgabe);

Das Beispiel zeigt, wie die Eigenschaften des Objekts document durchlaufen werden können, um beispielsweise die Fähigkeiten des jeweiligen Browsers zu ermitteln.

Abbildung 1.7: Ausgabe der Eigenschaften von document (Ausschnitt)

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.

1 for (i = 1; i < 11; i++) {
2   if (i == 5) continue;
3   console.log("I = " + i + " I*I = " + (i * i));
4   if (i == 9) {
5     break;
6   }
7 }

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.

1 let i, j;
2 outer: for (i = 0; i < 10; i++) {
3     inner: for (j = 0; j < 10; j++) {
4        if (j==3) continue inner;
5        if (j==6) continue outer;
6       document.write(i + " * " + j + " = " + (i * j) +
7                      "<br />");
8     }
9   }

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.

Abbildung 1.8: Ausgabe (Ausschnitt vom Anfang)

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.

 1 let fibonacci = {
 2   [Symbol.iterator]() {
 3     let pre = 0, cur = 1;
 4     return {
 5       next() {
 6         [pre, cur] = [cur, pre + cur];
 7         return { done: false, value: cur }
 8       }
 9     }
10   }
11 }
12 
13 for (var n of fibonacci) {
14   // Ende bei 1000
15   if (n > 1000)
16     break;
17   console.log(n);
18 }

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:

 1 interface IteratorResult {
 2   done: boolean;
 3   value: any;
 4 }
 5 interface Iterator {
 6   next(): IteratorResult;
 7 }
 8 interface Iterable {
 9   [Symbol.iterator](): Iterator
10 }

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.

 1 function ZeroDivError (msg) {
 2 
 3   this.name = 'ZeroDivError';
 4   this.message = msg === 'string' && msg.length != 0 ? msg :
 5        'Division durch Null!';
 6 
 7   this.toString = function () { return this.name + ': ' +
 8        this.message }
 9 }
10 
11 // Demo-Funktion
12 function div (a, b) {
13   if (b == 0)
14       throw new ZeroDivError ();
15   return a / b;
16 }
17 
18 // Abfangen von Exceptions
19 try {
20 
21   document.write ('10 / 2 = ' + div (10, 2) + '<br>');
22   document.write ('5 / 0 = ' + div (5, 0) + '<br>');
23 } catch (e) {
24   document.write ('Exception aufgetreten: ' + e);
25 }

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.

 1 function f() {
 2   {
 3     let x;
 4     {
 5       // Ein innerer (Block)-Scope
 6       const x = "sneaky";
 7       // Fehler, wegen `const` unveränderlich
 8       x = "foo";
 9     }
10     // Möglich, weil im Scope und durch `let` veränderlich
11     x = "bar";
12     // Fehler, erneute Deklaration im Scope unzulässig
13     let x = "inner";
14   }
15 }

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:

return expression;
return;

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.

Listing 2.1: Objekte nutzen
1 const person = new Object();
2 person.name = "Jörg Krause";
3 person.height = "180cm";
4 
5 person.run = function() {
6   this.state = "running";
7   this.speed = "4m/s";
8 }

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.

 1 const myObject = {
 2   property1: "Hello",
 3   property2: "World",
 4   property3: ["start", 1, 2, 3, "ende"],
 5 
 6   method1: function(){
 7               console.log("Methode wurde aufgerufen: " +
 8                            this.property1);
 9             }
10 };
11 myObject.method1();

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:

Abbildung 2.1: Ausgabe des Scripts

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.

 1 function Cat(name) {
 2    this.name = name;
 3    this.talk = function() {
 4       alert( this.name + " macht miau!" )
 5    }
 6 }
 7 
 8 // Verwendung
 9 const cat1 = new Cat("Juri");
10 cat1.talk(); //zeigt "Juri macht miau!"
11 
12 cat2 = new Cat("Wanja");
13 cat2.talk(); //zeigt "Wanja macht miau!"

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 Funktion myObject.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:

 1 function Cat(name) {
 2    this.name = name;
 3 }
 4 
 5 // Instanzen erstellen
 6 cat1 = new Cat("Juri");
 7 cat2 = new Cat("Wanja");
 8 
 9 Cat.prototype.talk = function() {
10    alert( this.name + " macht miau!" );
11 };
12 
13 cat1.talk(); //zeigt "Juri macht miau!"
14 cat2.talk(); //zeigt "Wanja macht miau!"

Anders sieht es aus, wenn es die Funktion bereits gibt:

 1 function Cat(name) {
 2    this.name = name;
 3    this.talk = function () { alert('stumm'); };
 4 }
 5 
 6 // Instanzen erstellen
 7 cat1 = new Cat("Juri");
 8 cat2 = new Cat("Wanja");
 9 
10 Cat.prototype.talk = function() {
11    alert( this.name + " macht miau!" );
12 };
13 
14 cat1.talk(); //zeigt "stumm"
15 cat2.talk(); //zeigt "stumm"

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.

Object.prototype.talk
|
+----Cat.prototype.talk ('macht miau')
     |
     +----cat1.talk ('stumm')

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.

1 Cat.prototype.changeName = function(name) {
2       this.name = name;
3 }
4 
5 const firstCat = new Cat("Juri");
6 firstCat.changeName("Wanja");
7 // zeigt "Wanja sagt miau!"
8 firstCat.talk();

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:

Listing 2.2: Getter und Setter (getset.js)
 1 var Rectangle = function (width, height) {
 2   this._width = width;
 3   this._height = height;
 4 };
 5 
 6 Rectangle.prototype = {
 7   set width (width) {
 8      this._width = width;
 9   },
10   get width () {
11     return this._width;
12   },
13   set height (height) {
14     this._height = height;
15   },
16   get height () {
17     return this._height;
18   },
19   get area () { return this._width * this._height; }
20 };
21 var r = new Rectangle(50, 20);
22 console.log(r.area);

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.

Listing 2.3: Private Mitglieder (private.js)
 1 function SomeObject(param) {
 2 
 3   function dec() {
 4     if (this.member > 0) {
 5       this.member -= 1;
 6       return true;
 7     } else {
 8       return false;
 9     }
10   }
11 
12   this.member = param;
13   dec();
14   dec();
15   return this;
16 }
17 
18 var o = SomeObject(10);
19 
20 console.log(o.member);

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:

o.dec();

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.

Listing 2.4: Privilegierte Funktionen (priv.js)
 1 function SomeObject(param) {
 2 
 3   function dec() {
 4     if (secret > 0) {
 5         secret -= 1;
 6         return true;
 7     } else {
 8         return false;
 9     }
10   }
11 
12   this.member = param;
13   var secret = 3;
14   var that = this;
15 
16   this.privileged = function () {
17     if (dec()) {
18         return that.member;
19     } else {
20         return null;
21     }
22   };
23 }
24 
25 var o = new SomeObject(10);
26 console.log(o.privileged());
27 console.log(o.privileged());
28 console.log(o.privileged());
29 console.log(o.privileged());

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:

1 var o1 = {};
2 o1.x = 3;
3 o1.y = 4;

Alternativ kann die Zuweisung der Eigenschaften auch sofort erfolgen:

1 var o2 = {
2   x : 3
3 };
4 
5 var o3 = {
6   first: o2,
7   second: function() { return 5; }
8 }

Das letzte Beispiel definiert second() als Methode. Alternativ zum Punkt kann auch folgende Schreibweise benutzt werden:

1 o2.x === 3;
2 o2["x"] === 3;
3 o2["the answer!"] = 42;

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:

1 var o1 = { };
2 o1.valueOf() // Ausgabe: "[object Object]"

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:

1 var o2 = {
2   valueOf: function() {
3     return 10;
4   }
5 };
6 o2.valueOf() // 10

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:

1 var point2d = { x:10, y:20 };
2 var point3d = Object.create(point2d);
3 point3d.z = 30;

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:

1 function create(parent) {
2   var F = function() {};
3   F.prototype = parent;
4   return new F();
5 }

Mit einer Konstruktorfunktion sieht das dagegen folgendermaßen aus:

1 function SomeConstructor() {
2 }
3 
4 var o = new SomeConstructor();

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)

 1 var obj = {
 2   sum: function(x,y) { return x + y; }
 3 }
 4 obj.sum(3,4);
 5 
 6 var obj2 = {
 7   offset: 7,
 8   sum: function(x, y) {
 9 	return x + y + this.offset;
10   }
11 }
12 
13 obj2.sum(3,4);
14 
15 var f = obj2.sum;
16 f(3,4) // NaN;
17 
18 f.call(obj2, 3, 4);

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.

 1 var Button = function(content) {
 2   this.content = content;
 3 };
 4 Button.prototype.click = function() {
 5   console.log(this.content);
 6 }
 7 
 8 var myButton = new Button('OK');
 9 myButton.click();
10 
11 var looseClick = myButton.click;
12 looseClick();
13 
14 var boundClick = myButton.click.bind(myButton);
15 boundClick();

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.

1 var john = {
2   name: 'Joerg',
3   greet: function(person) {
4     alert("Hallo " + person + ", ich bin " + name);
5   }
6 };
7 john.greet("Clemens");
8 // Ergibt: "Hallo Joerg, ich bin "

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:

 1 // Global, entspricht: window.name = 'Uwe';
 2 name = 'Uwe';
 3 let john = {
 4   name: 'Joerg',
 5   greet: function(person) {
 6     alert("Hallo " + person + ", ich bin " + name);
 7   }
 8 };
 9 john.greet("Clemens");
10 // => "Hallo Clemens, ich bin Uwe"

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:

1 var jk = {
2   name: 'Joerg',
3   greet: function(person) {
4     alert("Hallo " + person + ", ich bin " + this.name);
5   }
6 };
7 jk.greet("Clemens");
8 // => "Hallo Clemens, ich bin Joerg"

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.

Abbildung 2.2: Ausgabe des Scripts

Ein weiteres Beispiel zeigt, wie schnell auch this tückisch werden kann. Typisch sind Referenzen auf Funktionen, wie hier in Zeile 7:

1 var jk = {
2   name: 'Joerg',
3   greet: function(person) {
4     alert("Hallo " + person + ", ich bin " + this.name);
5   }
6 };
7 var fx = jk.greet;
8 fx("Clemens");
9 // Ergibt: "Hallo Clemens, ich bin "

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:

 1 function Person(first, last, age) {
 2   this.first = first;
 3   this.last = last;
 4   this.age = age;
 5 }
 6 Person.prototype = {
 7   getFullName: function() {
 8     alert(this.first + ' ' + this.last);
 9   },
10   greet: function(other) {
11     alert("Hallo " + other.first + ", ich bin " +
12     this.first + ".");
13   }
14 };

Nun wird dieser Code benutzt:

1 var jk = new Person('Joerg', 'Krause', 50);
2 var ck = new Person('Clemens', 'Krause', 25);
3 ck.greet(jk);

Das funktioniert soweit gut. Nun erfolgt eine Erweiterung:

1 function times(n, fx, arg) {
2   for (var index = 0; index < n; ++index) {
3     fx(arg);
4   }
5 }
6 times(3, ck.greet, jk); //
7 
8 times(1, jk.getFullName);
Abbildung 2.3: Erste Ausgabe (in Chrome via JSFiddle)
Abbildung 2.4. Zweite Ausgabe (in Chrome via JSFiddle), erscheint 3 mal
Abbildung 2.5: Dritte Ausgabe (in Chrome via JSFiddle), erscheint 1 mal

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:

1 this.items.each(function(item) {
2   // Verarbeitung
3   this.markItemAsProcessed(item);
4 });

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.

1 var fx = ck.greet;
2 fx.apply(ck, [jk]);

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.

1 var fx = ck.greet;
2 fx.call(ck, jk);

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:

1 function createBoundedWrapper(object, method) {
2   return function() {
3     return method.apply(object, arguments);
4   };
5 }

Das wird benutzt wie nachfolgend gezeigt:

1 var ckGreet = createBoundedWrapper(ck, ck.greet);
2 ckGreet(jk);

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.

Listing 2.5: Erstellen und Ableiten von Klassen (class1.js)
 1 class Person {
 2   constructor(name){
 3     this.name = name;
 4   }
 5 }
 6 class Student extends Person {
 7   constructor(course, name) {
 8     super(name);
 9 
10     this.course = course;
11     //...
12   }
13   show() {
14     return `${this.name} ist im Kurs ${this.course}`;
15   }
16   static defaultStudent() {
17     return new Student('Unbekannt', 'Steven Student');
18   }
19 }
20 var p = Student.defaultStudent();
21 console.log(p.show());

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:

1 class Polygon {
2   constructor() {
3     this.name = "Polygon";
4   }
5 }
6 
7 var poly1 = new Polygon();
8 
9 console.log(poly1.name);

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

Listing 2.6: Statische Mitglieder (static.js)
 1 class StaticMethodCall {
 2   static staticMethod() {
 3     return 'Statische Methode';
 4   }
 5   static anotherStaticMethod() {
 6     return this.staticMethod() + ' intern gerufen';
 7   }
 8 }
 9 StaticMethodCall.staticMethod();
10 
11 StaticMethodCall.anotherStaticMethod();

Der Zugriff kann auch im Konstruktor erfolgen.

Listing 2.7: Statischer Aufruf im Konstruktor (staticself.js)
 1 class StaticMethodCall {
 2   constructor() {
 3     console.log(StaticMethodCall.staticMethod());
 4 
 5     console.log(this.constructor.staticMethod());
 6   }
 7 
 8   static staticMethod() {
 9     return 'Statische Methode';
10   }
11 }

Vererbung mit extends

Wird das Objektkonstrukt mit class erstellt, so kann von einer anderen Klasse mit extends geerbt werden.

Listing 2.8: Vererbung steuern (extends.js)
 1 class Polygon {
 2     constructor() {
 3         this.name = "Polygon";
 4     }
 5 }
 6 
 7 class Square extends Polygon {
 8   constructor(length) {
 9     super(length, length);
10     this.name = 'Square';
11   }
12 
13   get area() {
14     return this.height * this.width;
15   }
16 
17   set area(value) {
18     this.area = value;
19   }
20 }

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.

Listing 2.9: Vererbung steuern (protoextends.js)
 1 class Polygon {
 2     constructor() {
 3         this.name = "Polygon";
 4     }
 5 }
 6 
 7 class Square extends Polygon {
 8     constructor() {
 9         super();
10     }
11 }
12 
13 class Rectangle {}
14 
15 Object.setPrototypeOf(Square.prototype, Rectangle.prototype);
16 
17 console.log(Object.getPrototypeOf(Square.prototype) === Polygon.p\
18 rototype);
19 console.log(Object.getPrototypeOf(Square.prototype) === Rectangle\
20 .prototype);
21 
22 let newInstance = new Square();
23 console.log(newInstance.name);

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:

Listing 2.10: Vererbung mit proto (proto.js)
 1 var a = {
 2   x: 10,
 3   calculate: function (z) {
 4     return this.x + this.y + z;
 5   }
 6 };
 7 
 8 var b = {
 9   y: 20,
10   __proto__: a
11 };
12 
13 var c = {
14   y: 30,
15   __proto__: a
16 };
17 
18 // Aufruf der geerbten Funktionen
19 b.calculate(30); // 60
20 c.calculate(40); // 80

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.

Listing 2.11: Ableitung eingebauter Typen (arrayext.js)
1 class MyArray extends Array {
2   constructor(...args) {
3     super(...args);
4   }
5 }
6 
7 var arr = new MyArray();
8 arr[1] = 12;
9 arr.length == 2

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:

1 setInterval('doSomethingPeriodically()', 1000);
2 setTimeout('doSomethingAfterFiveSeconds()', 5000);

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:

1 setInterval(doSomethingPeriodically, 1000);
2 setTimeout(doSomethingAfterFiveSeconds, 5000);

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:

1 setInterval(function() {
2    // tu was jede Sekunde
3 }
4 , 1000);
5 
6 setTimeout(function() {
7    // tu was nach 5 Sekunden
8 }, 5000);

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

1 var Zahl = Number.MAX_VALUE;
2 if (!isFinite(Zahl * 2)) {
3   alert("Die Zahl ist nicht zu verarbeiten.");
4 }

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()
1 var uri = 'https://somewebsite.de?p=First Param&q=Last Param';
2 var encoded = encodeURI(uri);
3 console.log(encoded);

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:

 1 var cnt = 0;
 2 function increment() {
 3   return cnt++;
 4 }
 5 function reset() {
 6   cnt = 0;
 7 }
 8 function show() {
 9   console.log(cnt);
10 }
11 reset();
12 increment();
13 increment();
14 show();

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:

 1 var jk_cnt = 0;
 2 function jk_increment() {
 3   return jk_cnt++;
 4 }
 5 function jk_reset() {
 6   jk_cnt = 0;
 7 }
 8 function jk_show() {
 9   console.log(jk_cnt);
10 }
11 jk_reset();
12 jk_increment();
13 jk_increment();
14 jk_show();

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.

 1 var jk_obj = { };
 2 jk_obj.cnt = 0; 
 3 jk_obj.increment = function () {
 4   return this.cnt++;
 5 };
 6 jk_obj.reset = function () {
 7   this.cnt = 0;
 8 };
 9 jk_obj.show = function () {
10   console.log(this.cnt);
11 };
12 jk_obj.increment();
13 jk_obj.increment();
14 jk_obj.show();
15 jk_obj.cnt = -100; // fatal

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:

1 (function () {
2     /* ... */
3 })();

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:

 1 (function() {   
 2   var cnt = 0; 
 3   function increment() {
 4     return cnt++;
 5   }
 6   function reset() {  
 7     cnt = 0;
 8   }
 9   function show() {
10     console.log(cnt);
11   }
12   reset();
13   increment();
14   increment();
15   show();
16 })();

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:

 1 var Modul = (function () {
 2 
 3     /* ... private Objekte ... */
 4 
 5     /* Gebe öffentliche API zurück: */
 6     return {
 7         öffentlicheMethode : function () { ... }
 8     };
 9 
10 })();

Auf das vorherige Beispiel angewendet sieht das nun folgendermaßen aus:

 1 var jk = function() {
 2   var cnt = 0;
 3   function reset() {
 4     cnt = 0;
 5   }
 6   reset();
 7 
 8   return {
 9     increment: function () {           
10       return cnt++;
11     },        
12     show:  function () {          
13       console.log(cnt);
14     } 
15   };
16 }();
17 
18 jk.increment();
19 jk.increment();
20 jk.show();

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.

 1 var jk = function() {    
 2   var counterInitialValue = 0;
 3   var counterConstructor = function() { 
 4      this.cnt = counterInitialValue;     
 5   };
 6   counterConstructor.prototype.increment = function() {       
 7      this.cnt++;
 8   };
 9   counterConstructor.prototype.show = function() {      
10     console.log(this.cnt);
11   };
12   return {
13     Counter : counterConstructor   
14   };
15 }();

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:

1 var mycnt = new jk.Counter();
2 mycnt.increment();
3 mycnt.increment();
4 mycnt.show();

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:

1 (function (window, document, undefined) {
2 
3     /* ... */
4 
5 })(window, document);

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:

 1 var MODULE = (function () {
 2    var my = {},
 3    privateVariable = 1;
 4    function privateMethod() {
 5      // ...
 6    }
 7    my.moduleProperty = 1;
 8    my.moduleMethod = function () {
 9      // ...
10    };
11    return my;
12 }());

In einem weiteren Skript kann dieses Modul erweitert werden:

1 var MODULE = (function (my) {
2 
3   my.anotherMethod = function () {
4     // neue (weitere) Methode
5   };
6 
7   return my;
8 
9 } (MODULE));

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:

1 var MODULE = (function (my) {
2 
3   // Mehr Funktionen
4 
5   return my;
6 } (MODULE || {}));

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.

1 var Namensraum = {};
2 Namensraum.Modul1 = (function () { ... })();
3 Namensraum.Modul2 = (function () { ... })();

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.

Listing 4.1: Exportiertes Modul (lib/math.js)
1 export function sum(x, y) {
2   return x + y;
3 }
4 export var pi = 3.141593;
Listing 4.2: Import als Namensraum (app.js)
1 import * as math from "lib/math";
2 console.log("2π = " + math.sum(math.pi, math.pi));
Listing 4.3: Import eines Moduls (otherapp.js)
1 import {sum, pi} from "lib/math";
2 console.log("2π = " + sum(pi, pi));

Das Exportieren (Bereitstellen) eines Moduls erfolgt mit export:

Listing 4.4: Standard exportieren (lib/mathplusplus.js)
1 export * from "lib/math";
2 export var e = 2.71828182846;
3 export default function(x) {
4     return Math.exp(x);
5 }

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.

Listing 4.5: Gemischer Import (app2.js)
1 import exp, {pi, e} from "lib/mathplusplus";
2 console.log("e^π = " + exp(pi));

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.

1 function f(x, y = 12) {
2   // y ist 12, wenn das Argument entfällt oder undefined ist
3   return x + y;
4 }
5 f(3) == 15;

Argument-Arrays

Eine beliebige Anzahl Argumente lässt sich mit dem ...-Operator an ein Array binden.

1 function f(x, ...y) {
2   // y ist nun ein Array
3   return x * y.length;
4 }
5 f(3, 'hello', true) == 6;

Das lässt sich auch umdrehen und beim Aufruf benutzen:

1 function f(x, y, z) {
2   return x + y + z;
3 }
4 // Das Array wird den Argumenten zugewiesen
5 f(...[1, 2, 3]) == 6;

Rest-Parameter

Der Parameter-Operator ... (auch Rest-Operator) stellt die Parameter als Array bereit.

 1 function sum(...theArgs) {
 2   return theArgs.reduce((previous, current) => {
 3     return previous + current;
 4   });
 5 }
 6 
 7 console.log(sum(1, 2, 3));
 8 // Ausgabe: 6
 9 
10 console.log(sum(1, 2, 3, 4));
11 // Ausgabe: 10

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:

 1 // Ausdrücke
 2 var odds = evens.map((v) => v + 1);
 3 var nums = evens.map((v, i) => v + i);
 4 
 5 // Anweisungen
 6 nums.forEach((v) => {
 7   if (v % 5 === 0) fives.push(v);
 8 });
 9 
10 // Benutzung von this
11 var bob = {
12   _name: 'Bob',
13   _friends: [],
14   printFriends() {
15     this._friends.forEach((f) => console.log(this._name + ' knows\
16  ' + f));
17   },
18 };
19 
20 // Argumente
21 function square() {
22   let example = () => {
23     let numbers = [];
24     for (let number of arguments) {
25       numbers.push(number * number);
26     }
27 
28     return numbers;
29   };
30 
31   return example();
32 }
33 
34 // Ausgabe: [4, 16, 56.25, 64, 132.25, 441]
35 square(2, 4, 7.5, 8, 11.5, 21);

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.

1 // knappe Syntax, implizierte Rückgabe
2 var func = (x) => x * x;
3 // mit Block-Rumpf, explizite Rückgabe wird benötigt
4 var func = (x, y) => {
5   return x + y;
6 };

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:

params => {object:literal}

Die Aufrufreihenfolge ist stattdessen:

1 var func = () => {  foo: 1  };
2 var func = () => {  foo: function() {}  };

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:

var func = () => ({ foo: 1 });

Zeilenumbruch

Lambda-Funktionen können keinen Zeilenumbruch zwischen Parametern und dem Pfeil haben.

1 var func = ()
2            => 1; // Syntaxfehler: Ausdruck erwartet, '=>' erhalten

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

1 let callback;
2 
3 callback = callback || function() {}; // ok
4 callback = callback || () => {};      // Syntaxfehler
5 callback = callback || (() => {});    // ok

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.

 1 var obj = {
 2   // Direkte Bestimmung des Prototypen
 3   __proto__: theProtoObj,
 4   // Benannte Eigenschaften setzen den Prototypen nicht
 5   ['__proto__']: somethingElse,
 6   // Kurzschreibweise für 'handler': handler
 7   handler,
 8   // Methoden
 9   toString() {
10     // mit Aufruf der Basisklassen mittels "super"
11     return 'd ' + super.toString();
12   },
13   // Berechnete Variablennamen
14   ['prop_' + (() => 42)()]: 42,
15 };

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.

 1 // Zuweisen einer Liste
 2 var [a, , b] = [1, 2, 3];
 3 a === 1;
 4 b === 3;
 5 
 6 // Objektbaum-Extraktion
 7 var {
 8   op: a,
 9   lhs: { op: b },
10   rhs: c,
11 } = getASTNode();
12 
13 // Kurzschreibweise
14 // bindet `op`, `lhs` and `rhs` im Scope
15 var { op, lhs, rhs } = getASTNode();
16 
17 // Parameterzuordnung
18 function g({ name: x }) {
19   console.log(x);
20 }
21 g({ name: 5 });
22 
23 // Destrukturierung ohne Eingabe
24 var [a] = [];
25 a === undefined;
26 
27 // Destrukturierung mit Standardwert
28 var [a = 1] = [];
29 a === 1;

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.

1 var myIterable = {};
2 myIterable[Symbol.iterator] = function* () {
3   yield 2;
4   yield 4;
5   yield 6;
6 };
7 var result = [...myIterable];
8 result.forEach((e) => console.log(e));

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:

1 const obj = {
2   *generatorMethod() {
3     // Implementierung
4   },
5 };
6 const genObj = obj.generatorMethod();

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.

1 function* yieldAllValuesOf(iterable) {
2   yield* iterable;
3 }

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:

1 const iterable = {
2     [Symbol.iterator](): iterator
3 }

iterator muss nun bestimmten Bedingungen gehorchen, um zu funktionieren. Konkret verlangt wird folgende Struktur:

1 const iterator = {
2   next() {
3     value: any,
4     done: boolean
5   }
6 }

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:

1 const array = ['foo', 'bar', 'zed'];
2 const iterator = array[Symbol.iterator]();
3 console.log(iterator.next());
4 console.log(iterator.next());
5 console.log(iterator.next());
6 console.log(iterator.next());

In Zeile 2 wird der Iterator des Arrays geholt. Dann kann man auf die Werte mit Aufrufen von next() zugreifen.

Abbildung 5.1: Ausgabe eines Array-Iterators

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:

 1 for (let pair of new Map([
 2   ['foo', 'Mr.Foo'],
 3   ['bar', 'Mr.Bar'],
 4 ])) {
 5   console.log(pair);
 6 }
 7 
 8 for (let e of new Set(['foo', 'bar'])) {
 9   console.log(e);
10 }
Abbildung 5.2: Ausgabe der Daten-Iteratoren

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 oder setImmediate
  • 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:

Listing 6.1: Asynchron mit setTimeout (async.js)
 1 function getList(callback) {
 2   setTimeout(function () { callback([1, 2, 3, 4, 5, 6]); }, 0);
 3 }
 4 function getEvens(list) {
 5   return list.filter(function (item) { return item % 2 == 0; });
 6 }
 7 function sum(list) {
 8   var res = getEvens(list);
 9   console.log(res.reduce(function (sum, n) { return (sum += n); }\
10 , 0));
11 }
12 
13 getList(sum);
14 console.log("Warte...");

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:

1 setTimeout(function () { callback([1, 2, 3, 4, 5, 6]); }, 2000);

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:

 1 function foo() {
 2   bar(function (res) {
 3     console.log("Argument = " + res);
 4   });
 5 }
 6 function bar(fn) {
 7   baz(fn);
 8 }
 9 function baz(fn) {
10   fn(3);
11 }
12 foo();

Die Funktionen bilden nun eine Sequenz, die synchron abgearbeitet wird, wie in Abbildung 6.1 gezeigt.

Abbildung 6.1: Sequenz der Aufrufe

Ein etwas konkreteres Beispiel (Listing 6.2) definiert mehrere Funktionen, die sequenziell ausgeführt werden:

Listing 6.2: Asynchron mit setTimeout (calc.js)
 1 function mul(x, y, cont) { cont(x * y); }
 2 function add(x, y, cont) { cont(x + y); }
 3 function sub(x, y, cont) { cont(x - y); }
 4 function div(x, y, cont) { cont(x / y); }
 5 function sqr(x, cont) { cont(x ^ 2); }
 6 
 7 var a = 1, b = 2, c = 3, d = 4, e = 5;
 8 
 9 sqr(b, (res) => {
10   mul(res, c, (res) => {
11     div(res, d, (res) => {
12       add(a, res, (res) => {
13         sub(res, e, (res) => {
14           console.log(res);
15         });
16       });
17     });
18   });
19 });

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:

1 function factorial(n) {
2   return n ? n * factorial(n-1) : 1;
3 }
4 factorial(32768); // Fehler: Range Error

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:

1 function factorial(n) {
2   function _factorial(n, acc) {
3     acc || (acc = 1);
4     // Endaufruf:
5     return n ? _factorial(n-1, n*acc) : acc;
6   }
7   return _factorial(n);
8 }

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:

1 function factorialL(n) {
2     var acc = 1;
3     while(n) {
4         acc *= n--;
5     }
6     return acc;
7 }

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:

1 function trampoline(fn) {
2     var args = [].slice.call(arguments, 1);
3     while (fn && fn instanceof Function) {
4         fn = fn.apply(this, args);
5     }
6     return fn;
7 }

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:

1 function factorial(n) {
2   function _factorial(n, acc) {
3     acc || (acc = 1);
4       return n ? _factorial.bind(this, n-1, n*acc) : acc;
5   }
6   return trampoline(_factorial, n);
7 }
8 
9 factorial(32768);

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:

 1 try {
 2   function async(cb) {
 3     setTimeout(function() {
 4       throw new Error("woops!");
 5       cb("done");
 6     }, 2000);
 7   }
 8 
 9   async(function(res) {
10       console.log("received:", res);
11   });
12 }
13 catch(e) {
14     console.log(e);
15 }

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:

 1 function async2(cb, err) {
 2   setTimeout(function() {
 3     try {
 4       if (true)
 5         throw new Error("Fehler!");
 6       else
 7         cb("done");
 8     }
 9     catch(e) {
10       err(e);
11     }
12   }, 2000);
13 }
14 
15 async2(function(res) {
16   console.log("Empfangen: ", res);
17 }, function(err) {
18   console.log("Fehler: Async erzeugte Ausnahme: ", err);
19 });

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.

 1 function asyncCPS(continuation) {
 2   setTimeout(function() {
 3     try {
 4       var res = 42;
 5       if (true)
 6         throw new Error("Fehler!");
 7       else
 8         continuation(null, res);
 9     }
10     catch(e) {
11       continuation(e, null);
12     }
13   }, 2000);
14 }
15 
16 asyncCPS(function(err, res) {
17   if (err) {
18     console.log("Fehler (cps): ", err);
19   } else {
20     console.log("(cps) passt: ", res);
21   }
22 });

Da dies häufiger benötigt wird, wäre eine Kapselung möglich:

 1 function TryCatch(fn, cont) {
 2   return function _tryWrapper(){
 3     var args = [].slice.call(arguments);
 4     try {
 5       fn.apply(this, args);
 6     }
 7     catch(e) {
 8       cont(e);
 9     }
10   };
11 }
12 
13 function asyncCPSWrap(continuation) {
14   setTimeout(TryCatch(() => {
15     var res = 42;
16     if (true)
17       throw new Error("woops!");
18     else
19       continuation(null, res);
20   }, continuation), 2000);
21 };
22 
23 asyncCPSWrap(function _callback(err, res) {
24   if (err) {
25     console.log("(cps) error:", err);
26   } else {
27     console.log("(cps) received:", res);
28   }
29 });

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:

1 var p2 = Promise.resolve("foo");
2 p2.then((res) => console.log(res));

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:

Listing 6.3: Promise mit Hilfsfunktion (promiseasync.js)
 1 var p = new Promise(function(resolve, reject) {
 2    setTimeout(() => resolve(4), 2000);
 3 });
 4 
 5 p.then((res) => {
 6   res += 2;
 7   console.log(res);
 8 });
 9 
10 p.then((res) => console.log(res));

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

Listing 6.4: Promise mit Hilfsfunktion (promisehelp.js)
 1 function timeout(duration = 0) {
 2   return new Promise((resolve, reject) => {
 3     setTimeout(resolve, duration);
 4   })
 5 }
 6 
 7 var p = timeout(1000).then(() => {
 8     return timeout(2000);
 9 }).then(() => {
10     throw new Error("Fehler");
11 }).catch(err => {
12     return Promise.all([timeout(100), timeout(200)]);
13 })

Hier wird der Konstruktor-Aufruf mit new benutzt, um ein Promise-Objekt zu erzeugen. Das Basisschema ist einfach:

1 var p = new Promise((resolve, reject) => {
2    if (/* Bedingung */) {
3       resolve(/* Wert */);  // Erfolg
4    }
5    else {
6       reject(/* Grund */);  // Fehler
7    }
8 });

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:

1 var p = new Promise((resolve, reject) => resolve(5));
2 p.then((val) => console.log(val)); // 5

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:

1 var fetchJSON = function(url) {
2   return new Promise((resolve, reject) => {
3     $.getJSON(url)
4       .done((json) => resolve(json))
5       .fail((xhr, status, err) => reject(status + err.message));
6   });
7 }

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.

 1 var itemUrls = {
 2     'http://www.api.com/items/1234',
 3     'http://www.api.com/items/4567'
 4   };
 5 var itemPromises = itemUrls.map(fetchJSON);
 6 
 7 Promise.all(itemPromises)
 8   .then(function(results) {
 9      results.forEach(function(item) {
10        // Verarbeitung
11      });
12   })
13   .catch(function(err) {
14     console.log("Fehler: ", err);
15   });

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.

 1 function delay(ms) {
 2   return new Promise((resolve, reject) => {
 3     setTimeout(resolve, ms);
 4   });
 5 }
 6 
 7 Promise.race([
 8   fetchJSON('http://www.api.com/profile/currentuser'),
 9   delay(5000).then(() => { user: 'guest' })
10 ])
11 .then(function(json) {
12    console.log("Daten: ", json.user);
13 })
14 .catch(function(err) {
15   console.log("Fehler: ", err);
16 });

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.

1 p.then((val) => console.log("Erfolg: ", val))
2  .catch((err) => console.log("Fehler: ", err));

Dies entspricht der Benutzung des zweiten Parameters im then, weshalb der folgende Code ein identisches Verhalten aufweist:

1 p.then((val) => console.log("fulfilled:", val))
2  .then(null, (err) => console.log("rejected:", err));

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:

 1 function resolveAfter2Seconds() {
 2   return new Promise(resolve => {
 3     setTimeout(() => {
 4       resolve('resolved');
 5     }, 2000);
 6   });
 7 }
 8 
 9 async function asyncCall() {
10   console.log('calling');
11   const result = await resolveAfter2Seconds();
12   console.log(result);
13 }
14 
15 asyncCall();

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ür values()
  • Set.prototype.values(): Die Werte in der Reihenfolge, in der sie eingefügt wurden
Listing 7.1: Set mit Ausgabe (set.js)
1 var mySet = new Set();
2 
3 mySet.add(1);
4 mySet.add(5);
5 mySet.add("Etwas Text");
6 var o = { a: 1, b: 2 };
7 mySet.add(o);
8 
9 for (let item of mySet) console.log(item);

Set verfügt nicht über typische Array-Funktionen wie map oder reduce. Bei kleinen Datenmengen eignet sich folgendes Konstrukt:

[...mySet].map(func);

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ür values()
  • [@@iterator]()

Es gibt nur wenige Eigenschaften:

  • size: Anzahl der Elemente
  • get Set[@@species]: Das Konstruktor-Symbol (entspricht new Set())

7.2 Map

Map ist dem Set sehr ähnlich, nur dass die Elemente hier Schlüssel-Werte-Paare sind.

const map = new Map([['uno', 'eins'], ['due', 'zwei']]);
map.set('tre', 'drei');

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.

 1 let arr = [3, 5, 7];
 2 arr.foo = "hallo";
 3 
 4 for (let i in arr) {
 5    console.log(i); // Ausgabe: "0", "1", "2", "foo"
 6 }
 7 
 8 for (let i of arr) {
 9    console.log(i); // Ausgabe: "3", "5", "7"
10 }

Der Iterator kann direkt abgerufen werden, um ein aufzählbares Objekt zu erhalten, dass mit for..of auswertbar ist:

 1 var map1 = new Map();
 2 
 3 map1.set('0', 'foo');
 4 map1.set(1, 'bar');
 5 
 6 var iterator1 = map1[Symbol.iterator]();
 7 
 8 for (let item of iterator1) {
 9   console.log(item);
10 }

Die Ausgabe lautet:

Array ["0", "foo"]
Array [1, "bar"]

Symbol.iterator ruft den Iterator ab.

Die Schleife lässt sich auch benutzen, um die Objekte gleich in Variablen zu zerlegen:

1 for (const [key, value] of myMap) {
2   console.log(`${key}: ${value}`);
3 }

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.

1 const buffer = new ArrayBuffer(8);
2 console.log(buffer.byteLength);

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.

 1 async function readImage(url) {
 2   let response = await fetch(url);
 3 
 4   if (response.ok) {
 5     let buffer = await response.arrayBuffer();
 6     // verarbeite Buffer
 7   } else {
 8     alert("HTTP-Error: " + response.status);
 9   }
10 }

Wird die Schreibweise mit then benutzt, sieht Zeile 5 folgendermaßen aus:

1 response.arrayBuffer().then(buffer => {
2   // verarbeite Buffer
3 });

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): Gibt true zurück wenn arg eines der Views des ArrayBuffers ist, wie zum Beispiel die typisierten Array-Objekte oder ein DataView. Ansonsten wird false zurückgegeben.
  • transfer(oldBuffer [, newByteLength]): Gibt einen neuen ArrayBuffer zurück, dessen Inhalt von den Daten des oldBuffers genommen wird und dann entweder abgeschnitten oder mit null auf newByteLength erweitert wird.
  • slice(begin, end): Gibt einen neuen ArrayBuffer 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: Wie Uint16Array, 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 eine for..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:

1 var buffer = new ArrayBuffer(8);
2 var view   = new Int32Array(buffer);

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:

^[_a-zA-Z0-9-]+(\.[_a-zA-Z0-9-]+)* @ [a-zA-Z0-9-]+\.([a-zA-Z]{2,3\
})$

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:

 1 var email = "joerg@krause.net";
 2 
 3 console.log(check(email));
 4 
 5 function check(email) {
 6   if (email.match(/^[_a-zA-Z0-9-]+(\.[_a-zA-Z0-9-]+)*@[a-zA-Z0-9-\
 7 ]+\.([a-zA-Z]{2,3})$/)) {
 8      return true;
 9   } else {
10      return false;
11   }
12 }

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 @:

1 ^[_a-zA-Z0-9-]+(\.[_a-zA-Z0-9-]+)*
2 [a-zA-Z0-9-]+\.([a-zA-Z]{2,3})$

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:

 1 ^                         // Beginn am Zeichenkettenanfang
 2   [_a-zA-Z0-9-]           // Zeichengruppe definieren
 3   +                       // Ein- oder mehrfach
 4     (                     // Gruppe 1
 5        \.                 // Ein "echter" Punkt
 6        [_a-zA-Z0-9-]      // Zeichengruppe
 7         +                 // Ein- oder mehrfach
 8     )                     // /* Ende Gruppe 1 */
 9     *                     // Gruppe null- oder mehrfach
10   @                       // ein @-Zeichen
11   [a-zA-Z0-9-]            // Zeichendefinition
12   +                       // Ein- oder mehrfach
13   \.                      // ein "echter" Punkt
14     (                     // Gruppe 2
15         [a-zA-Z]          // Zeichendefinition
16         {2,3}             // Zwei oder drei Zeichen
17     )                     // /* Ende Gruppe 2 */
18 $                         // Ende der Zeichenkette

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:

1 const patt = /abc/;
2 let s = /abc/.toString();
3 console.log(s);

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.

1 const patt = /^Auto$/;
2 console.log(patt.test("Auto"));
3 console.log(patt.test("Automatik"));
4 
5 const patt2 = /Auto/;
6 console.log(patt.test("Da ist unser Auto, ein VW."));
Abbildung 8.1: Ausgabe des Scripts

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

1 var patt = /.uto/;
2 console.log(patt.test("Auto"));
3 console.log(patt.test("Automatik"));
4 console.log(patt.test("Distributor"));
Abbildung 8.2: Ausgabe des Scripts

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.

1 var patt = /[a-f]+/;
2 console.log(patt.test("Auto"));
3 console.log(patt.test("42"));
4 console.log(patt.test("12 Tage"));
5 console.log(patt.test("borgen"));
Abbildung 8.3: Ausgabe des Scripts

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.

1 var patt = /[0-9:amp]/;
2 console.log(patt.test("12am"));
3 console.log(patt.test("4:17"));
Abbildung 8.4: Ausgabe des Scripts

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:

Tabelle: Abkürzung durch 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”:

1 <form>
2   <input type="text" name="postleitzahl" value="">
3   <input type="submit">
4 </form>

Dann wäre folgender Ausdruck geeignet:

 1 <script>
 2 var re = /^[0-9]{5}$/;
 3 var feld = "12683";
 4 var checkplz = re.exec(feld);
 5 if(!checkplz) {
 6   alert("Die Postleitzahl " + checkplz + " ist nicht korrekt.");
 7 } else {
 8     console.log(checkplz)
 9 }
10 </script>
Abbildung 8.5: Ausgabe des Scripts

Auf einen Blick

Tabelle: Wiederholungsoperatoren
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”

1 var patt = /(ab)+/;
2 console.log(patt.test("abc"));
3 console.log(patt.test("abcabc"));
4 console.log(patt.test("aacbb"));
Abbildung 8.6: Ausgabe des Scripts

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:

/^(["']){1}.*\1$/

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.

1 var patt = /^(["']){1}.*\1$/;
2 console.log(patt.test("\"Wort Wort Wort\""));
3 console.log(patt.test("\"Wort Wort Wort\'"));
4 console.log(patt.test("\'Wort Wort Wort\'"));
Abbildung 8.7: Ausgabe des Scripts

Das folgende Beispiel sucht einfach doppelte Wörter im Text:

/\b((\w+)\s+\2)+/

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.

1 var patt = /\b((\w+)\s+\2)+/;
2 console.log(patt.test("Script Script ist doppelt"));

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
1 const patt = /(\d{1,3})(?=d)/;
2 let text = "Dauer: 16d";
3 let test = patt.exec(text);
4 console.log("Tage: " + test[0]);
Abbildung 8.8: Ausgabe des Scripts

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 von true oder false
  • 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.

 1 var text = "Da sucht man alle Angaben von a ";
 2 var patt = /a/g;
 3 var match = patt.exec(text);
 4 console.log(match[0] + " gefunden bei Pos. " + match.index);
 5 
 6 match = patt.exec(text);
 7 do {
 8     match = patt.exec(text);
 9     if (!match) break;
10     console.log(match[0] +
11                 " gefunden bei Pos. " + match.index);
12 } while(true);

Die Ausgabe sieht nun folgendermaßen aus:

Abbildung 8.9: Ausgabe des Scripts

Eigenschaften

Einige Eigenschaften helfen dabei, mit den Informationen aus dem Ausdruck flexibel umgehen zu können:

  • constructor: Funktion, die das RegExp-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:

 1 var text = "Da sucht man alle Angaben von a ";
 2 var patt = /a/g;
 3 var match = patt.exec(text);
 4 console.log(match[0] + " gefunden bei Pos. " + match.index);
 5 
 6 match = patt.exec(text);
 7 do {
 8     match = patt.exec(text);
 9     if (!match) break;
10     console.log(match[0] + " gefunden bei Pos. " + match.index);
11     console.log( "Suche Weiter bei Pos. " + patt.lastIndex);
12 } while(true);

Die Ausgabe sieht nun folgendermaßen aus:

Abbildung 8.10: Ausgabe des Scripts

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

1 var patt = /(abc)|(def)/;
2 var text = "anton def";
3 console.log(patt.test(text));
4 console.log(RegExp.$1);

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:

1 var patt = /web/i;
2 patt.test("Besuche unsere Web-Kurse!");

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:

/web/i.exec("Besuche unsere Web-Kurse!");

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:

1 let patt =  new RegExp("pattern");

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:

let pattern = /[A-Z]/i;

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
1 var str = "Besuche unsere Web-Kurse";
2 var res = str.search(/Web/i);
3 console.log(res);

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:

1 var str = "Besuche unsere Web-Kurse";
2 var res = str.match(/Web/);
3 if (res) {
4   console.log("Treffer");
5 } else {
6   console.log("Kein Treffer");
7 }

Der Ausdruck ist erfüllt und es wird Treffer ausgegeben.

Analog funktioniert replace:

1 var str = "Besuche unsere Web-Kurse";
2 var res = str.replace(/Web/i, ".NET");
3 console.log(res);

Die Variable res enthält den Wert Besuche unsere .NET-Kurse.

8.8 Zusammenfassung

Hier finden Sie die Metazeichen und Symbole auf einen Blick.

Tabelle: Zeichen
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
Tabelle: Gruppen
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
Tabelle: Operatoren
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
Tabelle: Abkürzungen
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
Tabelle: JavaScript-Funktionen
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)
Tabelle: Literal-Optionen
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:

 1 var O = {a: 1};
 2 Object.defineProperty(O, 'b', {value: 2});
 3 O[Symbol('c')] = 3;
 4 
 5 Reflect.ownKeys(O); // ['a', 'b', Symbol(c)]
 6 
 7 function Calc(a, b){
 8   this.result = a + b;
 9 }
10 var instance = Reflect.construct(Calc, [20, 22]);
11 Console.log(instance.result); // 42

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:

1 try {
2   Object.defineProperty(target, 'foo', { value: 'bar' })
3   // Erfolg
4 } catch (e) {
5   // Fehler
6 }

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.

1 var success = Reflect.defineProperty(target, 'foo', { value: 'bar\
2 ' })
3 if (success) {
4   // Erfolg
5 } else {
6   // Fehler
7 }

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.

1 var target = { foo: 'bar', baz: 'wat' }
2 delete target.foo
3 console.log(target)

Ab ES2015 gibt es nun die Methode Reflect.deleteProperty.

1 var target = { foo: 'bar', baz: 'wat' }
2 Reflect.deleteProperty(target, 'foo')
3 console.log(target)

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.

apply(null, ['.foo', '.bar'])
apply.call(null, '.foo', '.bar')

Besser geht es mit dem Spread-Operator:

new MyCtor(...args)

Das Objekt Reflect bietet auch dafür eine Alternative:

Reflect.construct(Dominus, args)

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:

fn.apply(ctx, [1, 2, 3])

Das Risiko besteht darin, dass ein sinnfreier, fremder Kontext benutzt wird. JavaScript bietet hier keinen Schutz.

Etwas Schutz bietet eine weitere Überladungsform:

Function.prototype.apply.call(fn, ctx, [1, 2, 3])

All das beachtet der Spread-Operator in ES6:

fn(...[1, 2, 3])

Nun ist aber die Möglichkeit, this zu manipulieren, nicht mehr gegeben. An dieser Stelle greift nun eine weitere Methode des Reflect-Objekts:

Reflect.apply(fn, ctx, args);

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:

1 var handler = {
2   get () {
3     return Reflect.get(...arguments)
4   }
5 }
6 var target = { a: 'b' }
7 var proxy = new Proxy(target, handler)
8 console.log(proxy.a)

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:

1 var handler = {
2   get: Reflect.get
3 }

Diese Technik, Codeflüsse zu untersuchen, wird als Falle (trap) bezeichnet. Die generelle Syntax lautet:

return Reflect[trapName](...arguments)

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.

 1 declare type MethodDecorator =
 2    <T>(target: Object, propertyKey: string | symbol,
 3        descriptor: TypedPropertyDescriptor<T>)
 4             => TypedPropertyDescriptor<T> | void;
 5 
 6 declare type ClassDecorator =
 7   <TFunction extends Function>(target: TFunction)
 8             => TFunction | void;
 9 
10 declare type PropertyDecorator =
11   (target: Object, propertyKey: string | symbol)
12             => void;
13 
14 declare type ParameterDecorator =
15   (target: Object, propertyKey: string | symbol, parameterIndex: \
16 number)
17             => void;

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:

Listing 10.1: funcdec.js
 1 function log(target, key, value) {
 2   return {
 3     value: function (...args) {
 4       var a = args.map(a => JSON.stringify(a)).join();
 5       var result = value.value.apply(this, args);
 6       var r = JSON.stringify(result);
 7       console.log(`Call: ${key}(${a}) => ${r}`);
 8       return result;
 9     }
10   };
11 }

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):

1 class Example {
2   @log
3   oneFunction(n) {
4     return n * 2;
5   }
6 }

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:

 1 function log(target, key, descriptor) {
 2     return {
 3         value: function () {
 4             var args = [];
 5             for (var _i = 0; _i < arguments.length; _i++) {
 6                 args[_i] = arguments[_i];
 7             }
 8             var a = args.map(function (a) {
 9                               return JSON.stringify(a);
10                            }).join();
11             var result = descriptor.value.apply(this, args);
12             var r = JSON.stringify(result);
13             console.log("Call: " + key + "(" + a + ") => " + r);
14             return result;
15         }
16     };
17 }
18 var Example = /** @class */ (function () {
19     function Example() {
20     }
21     Example.prototype.oneFunction = function (n) {
22         return n * 2;
23     };
24     __decorate([
25         log
26     ], Example.prototype, "oneFunction", null);
27     return Example;
28 }());

Der Dekorator ist also nichts weiter, als eine dynamisch hinzugefügte Eigenschaft:

 1 Object.defineProperty(
 2   __decorate(
 3     [log],                    // Dekorator
 4     Beispiel.prototype,       // Zielmethode
 5     "oneFunction",            // Methodenname
 6                               // Beschreibung
 7     Object.getOwnPropertyDescriptor(Example.prototype, "oneFuncti\
 8 on")
 9   );
10 );

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):

 1 var __decorate = (this && this.__decorate)
 2      || function (decorators, target, key, desc) {
 3   var c = arguments.length;
 4   var r = c < 3 ? target
 5                 : desc === null ? desc = Object.getOwnPropertyDes\
 6 criptor(target, key)
 7                                 : desc, d;
 8   if (typeof Reflect === "object"
 9    && typeof Reflect.decorate === "function") {
10     r = Reflect.decorate(decorators, target, key, desc);
11   } else  {
12     for (var i = decorators.length - 1; i >= 0; i--) {
13        if (d = decorators[i]) {
14           r = (c < 3 ? d(r)
15                      : c > 3 ? d(target, key, r)
16                              : d(target, key)) || r;
17        }
18     }
19   }
20   return c > 3 && r && Object.defineProperty(target, key, r), r;
21 };

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:

1 __decorate(
2   [log],                       // Dekorator
3   Beispiel.prototype,          // Zielmethode
4   "oneFunction",               // Methodenname
5                                // Beschreibung:
6   Object.getOwnPropertyDescriptor(Example.prototype, "oneFunction\
7 ")
8 );

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:

1 [log].reduceRight(function(log, desc) {
2   if(log) {
3     return log(Beispiel.prototype, "oneFunction", desc);
4   }
5   else {
6     return desc;
7   }
8 }, Object.getOwnPropertyDescriptor(Beispiel.prototype, "oneFuncti\
9 on"));

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:

 1 function log(target, key, value) {
 2   return {
 3     value: function (...args) {
 4       var a = args.map(a => JSON.stringify(a)).join();
 5       var result = value.value.apply(this, args);
 6       var r = JSON.stringify(result);
 7       console.log(`Call: ${key}(${a}) => ${r}`);
 8       return result;
 9     }
10   };
11 }

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:

declare type PropertyDecorator =
        (target: Object, propertyKey: string | symbol) => void;

Die Platzierung erfolgt nun auf einer Eigenschaft:

 1 class Person {
 2 
 3   @logProperty
 4   public name;
 5   public surname;
 6 
 7   constructor(name, surname) {
 8     this.name = name;
 9     this.surname = surname;
10   }
11 }

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.

 1 var Person = (function () {
 2     function Person(name, surname) {
 3         this.name = name;
 4         this.surname = surname;
 5     }
 6     __decorate([
 7         logProperty
 8     ], Person.prototype, "name");
 9     return Person;
10 })();

Auch wird hier nicht mehr der Rückgabewert benutzt, es fehlt das return-Schlüsselwort.

Hier nun die eigentliche Implementierung des Dekorators:

Listing 10.4: propdec.js (Erster Teil)
 1 function logProperty(target, key) {
 2   var _val = this[key];
 3   var getter = function () {
 4     console.log(`Get: ${key} => ${_val}`);
 5     return _val;
 6   };
 7   var setter = function (newVal) {
 8     console.log(`Set: ${key} => ${newVal}`);
 9     _val = newVal;
10   };
11   if (delete this[key]) {
12     Object.defineProperty(target, key, {
13       get: getter,
14       set: setter,
15       enumerable: true,
16       configurable: true
17     });
18   }
19 }

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):

Set: name => Joerg
Get: name => Joerg

10.4 Dekoratoren für Klassen

Die Deklaration soll hier wieder als Ausgangspunkt dienen:

declare type ClassDecorator =
        <TFunction extends Function>(target: TFunction)
           => TFunction | void;

Der Einsatz erfolgt nun auf der Klasse selbst:

Listing 10.5: classdec.js (Zweiter Teil)
 1 @logClass
 2 class OtherPerson {
 3 
 4   public name;
 5   public surname;
 6 
 7   constructor(name, surname) {
 8     this.name = name;
 9     this.surname = surname;
10   }
11 }

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

Listing 10.6: Klassen-Dekorator classdec.js (Erster Teil)
 1 function logClass(target) {
 2   // Konstruktor merken
 3   var original = target;
 4   // Unterstützung der Instanziierung
 5   function construct(constructor, args) {
 6     var c = function () {
 7       return constructor.apply(this, args);
 8     }
 9     c.prototype = constructor.prototype;
10     return new c();
11   }
12   // Unterstützung des Konstruktors
13   var f = function (...args) {
14     console.log("New: " + original.name);
15     return construct(original, args);
16   }
17   // Ursprüngliche Vererbung kopieren
18   f.prototype = original.prototype;
19   // Neuer Konstruktor
20   return f;
21 }

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.

1 var me = new Person("Remo", "Jansen");
2 if (me instanceof Person){
3   console.log('Person');
4 }

Dekorator für Parameter

Auch Parameter lassen sich dekorieren. Erneut soll die Signatur als Ausgangspunkt dienen:

declare type ParameterDecorator =
    (target: Object,
     propertyKey: string | symbol,
     parameterIndex: number)
     => void;

Der Einsatz auf der bereits mehrfach benutzten Klasse Person sieht folgendermaßen aus (Zeile 11):

Listing 10.7: paramdec.js (Zweiter Teil)
 1 class ParamPerson {
 2   public name;
 3   public surname;
 4 
 5   constructor(name, surname) {
 6     this.name = name;
 7     this.surname = surname;
 8   }
 9 
10   public saySomething(@logParameter something) {
11     return this.name + " " + this.surname + " says: " + something;
12   }
13 }

Im folgende Beispiel ist der Index des Parameters in Zeile 2 gleich 0, in Zeile 5 dagegen ist es 1:

1 class foo {
2   public foo(@logParameter foo) {
3     return "bar";
4   }
5   public foobar(foo, @logParameter bar) {
6     return "foobar";
7   }
8 }

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:

Listing 10.8: paramdec.js (Erster Teil)
1 function logParameter(target, key, index) {
2   var metadataKey = `log_${key}_parameters`;
3   if (Array.isArray(target[metadataKey])) {
4     target[metadataKey].push(index);
5   } else {
6     target[metadataKey] = [index];
7   }
8   console.log(metadataKey);
9 }

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.

 1 class Person {
 2 
 3   public name;
 4   public surname;
 5 
 6   constructor(name, surname) {
 7     this.name = name;
 8     this.surname = surname;
 9   }
10 
11   @logMethod()
12   public saySomething(@logParameter() something) {
13     return this.name + " " + this.surname + " says: " + something;
14   }
15 }

Der dazu angepasste Dekorator der Methode sieht nun folgendermaßen aus:

 1 function logMethod(target, key, descriptor) {
 2   var originalMethod = descriptor.value;
 3   descriptor.value = function (...args) {
 4 
 5     var metadataKey = `__log_${key}_parameters`;
 6     var indices = target[metadataKey];
 7 
 8     if (Array.isArray(indices)) {
 9 
10       for (var i = 0; i < args.length; i++) {
11 
12         if (indices.indexOf(i) !== -1) {
13 
14           var arg = args[i];
15           var argStr = JSON.stringify(arg) || arg.toString();
16           console.log(`${key} arg[${i}]: ${argStr}`);
17         }
18       }
19       var result = originalMethod.apply(this, args);
20       return result;
21     }
22     else {
23 
24       var a = args.map(a => (JSON.stringify(a) || a.toString())).\
25 join();
26       var result = originalMethod.apply(this, args);
27       var r = JSON.stringify(result);
28       console.log(`Call: ${key}(${a}) => ${r}`);
29       return result;
30     }
31   }
32   return descriptor;
33 }

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:

 1 @logClass()
 2 class Person {
 3 
 4   @logProperty()
 5   public name;
 6 
 7   public surname;
 8 
 9   constructor(name, surname) {
10     this.name = name;
11     this.surname = surname;
12   }
13 
14   @logMethod()
15   public saySomething(@logParameter() something) {
16     return this.name + " " + this.surname + " says: " + something;
17   }
18 }

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:

Listing 10.9: alldec.js (Alternative)
 1 @logAll
 2 class AllPerson {
 3 
 4   @logAll
 5   public name;
 6 
 7   @logAll
 8   public surname;
 9 
10   constructor(n, sn) {
11     this.name = n;
12     this.surname = sn;
13   }
14 
15   @logAll
16   public saySomething(@logAll something) {
17     return this.name + " " + this.surname + " says: " + something;
18   }
19 }
20 
21 const allPerson = new AllPerson("Alwin", "All");
22 allPerson.saySomething("Hallo");

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:

Listing 10.10: alldec.js (Fortsetzung)
 1 function logAll(...args) {
 2   // Fix für Property
 3   if (args.length === 3 && args[2] === undefined){
 4     args.pop();
 5   }
 6   switch(args.length) {
 7     case 1:
 8       return logAllClass.apply(args[0], args);
 9     case 2:
10       return logAllProperty.apply(args[0], args);
11     case 3:
12       if(typeof args[2] === "number") {
13         return logAllParameter.apply(args[0], args);
14       }
15       return logAllMethod.apply(args[0], args);
16     default:
17       throw new Error("Decorators are not valid here! ");
18   }
19 }

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:

Listing 10.11: factorydec.js (Erster Teil)
 1 @logClassWithArgs("Ausgabe")
 2 class FactoryPerson {
 3 
 4   public name;
 5   public surname;
 6 
 7   constructor(name, surname) {
 8     this.name = name;
 9     this.surname = surname;
10   }
11 }

Hier soll eine Fabrikfunktion als Muster für die Implementierung dienen:

 1 function logClassWithArgs(comment) {
 2   return (target: any) => {
 3       // Konstruktor merken
 4     var original = target;
 5     // Unterstützung der Instanziierung
 6     function construct(constructor, args) {
 7       var c : any = function () {
 8         return constructor.apply(this, args);
 9       }
10       c.prototype = constructor.prototype;
11       return new c();
12     }
13     var f : any = function (...args) {
14       console.log(`New: ${original.name} ${comment}`);
15       return construct(original, args);
16     }
17     // Ursprüngliche Vererbung kopieren
18     f.prototype = original.prototype;
19     // Neuer Konstruktor
20     return f;
21   }
22 }

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:

Listing 10.2: Einfacher Proxy (proxy.js)
1 var target = {};
2 var handler = {
3   get: function (receiver, name) {
4     return `Hallo, ${name}!`;
5   }
6 };
7 
8 var p = new Proxy(target, handler);
9 console.log(p.Welt);

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:

Hallo, Welt!

Bei Funktionen sieht es ähnlich aus. Statt get wird hier apply benutzt:

Listing 10.3: Proxy mit apply (applyproxy.js)
 1 var target = function () {
 2   return "Ich bin das Ziel";
 3 };
 4 var handler = {
 5   apply: function (receiver, ...args) {
 6     console.log("Interceptor");
 7     return receiver(args);
 8   }
 9 };
10 
11 var p = new Proxy(target, handler);
12 console.log(p());

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:

Listing 10.4: Proxy mit Zuweisung (simpleproxy.js)
1 var target = {};
2 var handler = {};
3 var proxy = new Proxy(target, handler);
4 proxy.a = 'b';
5 console.log(target.a); // 'b'
6 console.log(proxy.c);  // undefined

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.

Listing 10.5: Proxy mit get (getproxy.js)
 1 var handler = {
 2   get (target, key) {
 3     console.info(`Get auf Eigenschaft "${key}"`);
 4     return target[key];
 5   }
 6 }
 7 var target = {};
 8 var proxy = new Proxy(target, handler);
 9 proxy.a = 'b';
10 console.log(proxy.a) // <- 'Zugriff auf "a"'
11 console.log(proxy.a) // <- 'Zugriff auf "b"'

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.

Listing: Proxy mit set (setproxy.js)
 1 var handler = {
 2   get (target, key) {
 3     invariant(key, 'get');
 4     return target[key];
 5   },
 6   set (target, key, value) {
 7     invariant(key, 'set');
 8     return true;
 9   }
10 }
11 function invariant (key, action) {
12   if (key[0] === '_') {
13     throw new Error(`Unerlaubter Zugriff ${action}
14                      auf private Eigenschaft "${key}"`);
15   }
16 }
17 var target = {
18   a: 'b',
19   _prop: 'secret'
20 };
21 var proxy = new Proxy(target, handler);
22 console.log(proxy.a); // Ergibt: 'b'
23 // Diese Zugriffe führen zu einem Fehler:
24 proxy._prop;
25 proxy._prop = 'c';

Die Ausgabe in Abbildung 10.1 zeigt, wie das Skript reagiert.

Abbildung 10.1: Ausgabe des Beispiels

Das ganze Konstrukt lässt sich noch in eine Hilfsfunktion makeProxy verpacken und dann bequem nutzen:

Listing 10.6: Proxy mit Hilfsfunktion (makeproxy.js)
 1 function makeProxy() {
 2   var target = {}
 3   var handler = {
 4     get (target, key) {
 5       invariant(key, 'get')
 6       return target[key]
 7     },
 8     set (target, key, value) {
 9       invariant(key, 'set')
10       return true
11     }
12   }
13   return new Proxy(target, handler)
14 }
15 function invariant (key, action) {
16   if (key[0] === '_') {
17     throw new Error(`Invalid attempt to ${action}
18                      private "${key}" property`)
19   }
20 }

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.

Listing 10.7: Proxy zum Validieren (valproxy.js)
 1 var validator = {
 2   set (target, key, value) {
 3     if (key === 'age') {
 4       if (typeof value !== 'number' || Number.isNaN(value)) {
 5         throw new TypeError('Alter muss eine Zahl sein');
 6       }
 7       if (value <= 0) {
 8         throw new TypeError('Alter muss positiv sein');
 9       }
10     }
11     return true
12   }
13 }
14 var person = { age: 27 };
15 var proxy = new Proxy(person, validator);
16 proxy.age = 'foo'; // Fehler
17 proxy.age = NaN;   // Fehler
18 proxy.age = 0;     // Fehler
19 proxy.age = 28;    // Korrekt
20 console.log(person.age); // 28

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:

Listing 10.1: Pseudo-Code einer validierenden Klasse
1 class Person {
2   @IsNumber('Alter muss eine Zahl sein')
3   @Range(0, 99, 'Alter muss positiv sein')
4   age: 0;
5 }

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.

1 var target = {};
2 var handler = {};
3 var {proxy, revoke} = Proxy.revocable(target, handler);
4 proxy.a = 'b';
5 console.log(proxy.a) // 'b';
6 revoke();
7 revoke(); // Egal
8 revoke(); // Egal
9 console.log(proxy.a); // Fehler

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:
1 Object.getPrototypeOf(target),
2 Reflect.getPrototypeOf(target),
3 target.__proto__,
4 object.isPrototypeOf(target),
5 object instanceof target
  • setPrototypeOf:
1 Object.setPrototypeOf(target),
2 Reflect.setPrototypeOf(target)
  • 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

1 const items = [12, 548, 'Doe' , 2145 , 119];
2 
3 var  randomItem = items[Math.floor(Math.random() * items.length)];

Zufallszahl aus einem Bereich

1 let x = Math.floor(Math.random() * (max - min + 1)) + min;

Array mit fortlaufenden Zahlen

1 var numbersArray = [] , max = 100;
2 
3 for( var i=1; numbersArray.push(i++) < max;);
4 
5 // Ausgabe: [1,2,3 ... 100]

Set mit Zeichen erzeugen

1 function generateRandomAlphaNum(len) {
2     var rdmString = "";
3     for( ; rdmString.length < len; rdmString  += Math.random().to\
4 String(36).substr(2));
5     return  rdmString.substr(0, len);
6 }

Zahlenarray durcheinander bringen

1 var numbers = [5, 458 , 120 , -215 , 228 , 400 , 122205, -85411];
2 numbers = numbers.sort(function(){ return Math.random() - 0.5});

Leerzeichen entfernen (trim)

1 String.prototype.trim = function() {
2   return this.replace(/^s+|s+$/g, "");
3 };

Neuere JavaScript-Umgebungen haben trim bereits eingebaut.

Arrays kombinieren

1 var array1 = [12 , "foo" , {name "Joe"} , -2458];
2 var array2 = ["Doe" , 555 , 100];
3 
4 Array.prototype.push.apply(array1, array2);

Argumente in Array konvertieren

1 var argArray = Array.prototype.slice.call(arguments);

Wert auf Zahl testen

1 function isNumber(n){
2     return !isNaN(parseFloat(n)) && isFinite(n);
3 }

Wert auf Array testen

1 function isArray(obj){
2     return Object.prototype.toString.call(obj) === '[object Array\
3 ]' ;
4 }

Neuere Browser kennen Array.isArray(obj); und benötigen dies nicht mehr.

Minimum und Maximum in einem Array

1 const numbers = [5, 458 , 120 , -215 , 228 , 400 , 122205, -85411\
2 ];
3 var maxInNumbers = Math.max.apply(Math, numbers);
4 var minInNumbers = Math.min.apply(Math, numbers);

Array leeren

1 var myArray = [12 , 222 , 1000 ];
2 myArray.length = 0; // myArray will be equal to [].

Array-Element entfernen

Folgendes geht nicht, da delete das Element auf undefined setzt, aber nicht entfernt.

1 var items = [12, 548 ,'a' , 2 , 5478 , 'foo' , 8852, , 'Doe' ,215\
2 4 , 119 ];
3 items.length; // return 11
4 delete items[3]; // return true
5 items.length; // return 11

Folgendes funktioniert:

1 var items = [12, 548 ,'a' , 2 , 5478 , 'foo' , 8852, , 'Doe' ,215\
2 4 , 119 ];
3 items.length; // return 11
4 items.splice(3,1) ;
5 items.length; // return 10

Array abschneiden

1 var myArray = [12 , 222 , 1000 , 124 , 98 , 10 ];
2 myArray.length = 4; // Ergibt: [12 , 222 , 1000 , 124]
1 myArray.length = 10; // the new array length is 10
2 myArray[myArray.length - 1] ; // undefined

Runden

1 var num =2.443242342;
2 num = num.toFixed(4);  // num ergibt: 2.4432

Gleitkommetipps

1 0.1 + 0.2 === 0.3    // ist false
2 9007199254740992 + 1 // ist 9007199254740992
3 9007199254740992 + 2 // ist 9007199254740994

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

1 for (var name in object) {
2     if (object.hasOwnProperty(name)) {
3         // Aktion
4     }
5 }

Dies ist vor allem sinnvoll, wenn das Basisobjekt sehr groß ist, beispielsweise ist dies bei HTML-Elementen der Fall.

Der Komma-Operator

1 var a = 0;
2 var b = ( a++, 99 );
3 console.log(a);  // a will be equal to 1
4 console.log(b);  // b is equal to 99

Prüfe bevor isFinite() benutzt wird

1 isFinite(0/0) ; // false
2 isFinite("foo"); // false
3 isFinite("10"); // true
4 isFinite(10);   // true
5 isFinite(undefined);  // false
6 isFinite();   // false
7 isFinite(null);  // true!

Vermeide negative Indizes in Arrays

1 var numbersArray = [1,2,3,4,5];
2 var from = numbersArray.indexOf("foo") ;  // ergibt: -1
3 numbersArray.splice(from,2);    // ergibt: [5]

for in ist nicht gut in Arrays

Folgendes ist keine gute Idee:

1 var sum = 0;
2 for (var i in arrayNumbers) {
3     sum += arrayNumbers[i];
4 }

So sollte es aussehen:

1 var sum = 0;
2 for (var i = 0, len = arrayNumbers.length; i < len; i++) {
3     sum += arrayNumbers[i];
4 }

Folgendes ist auch keine so gute Idee:

1 for (var i = 0; i < arrayNumbers.length; i++)

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