35 4 JavaScript als funktionale Programmiersprache Obwohl der Kern von JavaScript relativ einfach ist, folgt JavaScript keinem strengen Programmierparadigma. In diesem Kapitel wird JavaScript als funktionale Programmiersprache dargestellt, denn ohne die funktionalen Aspekte lassen sich die weiteren Aspekte der Sprache nur schwer beschreiben. Funktionen werden verwendet, um Anweisungen zusammenzufassen. In Funktionen zusammengefasste Anweisungen dienen der Wiederverwendung und der Strukturierung des Codes. In JavaScript sind Funktionen Sprachbestandteile erster Ordnung. Sie sind Objekte. Wie jedes andere Objekt kann man eine Funktion einer Variablen zuweisen, anderen Funktionen als Parameter übergeben oder als Rückgabewert zurückgeben. Weil Funktionen Objekte sind, können sie selbst auch weitere Funktionen und Eigenschaften (Member-Variablen bzw. Properties) enthalten. 4.1 Funktionsliteral Funktionsliterale erzeugen Funktionsobjekte. Funktionsobjekte sind echte JavaScript-Objekte. Sie sind eine Instanz des Function-Objekts. Das Funktionsliteral wird durch das reservierte Wort function eingeleitet. Danach folgt ein optionaler Funktionsname. Wenn ein Funktionsname angegeben wird, spricht man statt vom Funktionsliteral auch von der Funktionsanweisung. Der Name der Funktion wird in der name-Eigenschaft des Funktionsobjekts gespeichert. Die Parameter einer Funktion werden in Klammern als eine kommaseparierte Liste übergeben. Diese Parameter können in der Funktion als Variablen verwendet werden. Die Anzahl der Parameter stehen im Funktionsobjekt als lengthEigenschaft zur Verfügung. Der Rumpf der Funktion folgt den Parametern in geschweiften Klammern. function plus(x, y) { return x + y; } print(plus(1,2)); // 3 36 4 JavaScript als funktionale Programmiersprache Der Aufruf einer Funktion unterbricht den Programmablauf (das Programm selbst wird natürlich nicht unterbrochen) und übergibt die Kontrolle an die Funktion. Der Programmablauf wird fortgesetzt, wenn die Funktion über eine returnAnweisung beendet wird oder wenn die letzte Zeile der Funktion erreicht ist. Die return-Anweisung kann also auch verwendet werden, um eine Funktion vorzeitig zu beenden. Wenn keine return-Anweisung vorhanden ist, dann wird von der Funktion undefined zurückgegeben. Funktionen als Konstuktor-Funktionen aufrufen Achtung: Wenn die Funktion wie ein Konstruktor-Objekt (siehe folgendes Kapitel!) über die new-Anweisung aufgerufen wird, dann wird statt undefined das erzeugte Objekt zurückgegeben, falls die Funktion selbst keine Objekt als Rückgabewert hat! 4.2 Funktionsnamen Als Funktionsnamen sind alle gültigen JavaScript-Bezeichner1 möglich. Es ist aber eine Konvention, den Namen der Funktion mit einem Verb in Kleinbuchstaben zu beginnen und CamelCase-Schreibweise für zusammengesetzte Namen zu verwenden. Interne Funktionen, die nicht zu einer öffentlichen API gehören, werden per Konvention von einem Unterstrich »_« eingeleitet. 4.3 Parameter und Parametervalidierung Funktionen lassen sich in JavaScript nicht anhand der Parameter überladen. Dies bedeutet, dass die Funktion auch dann aufgerufen wird, wenn mehr oder weniger Parameter übergeben werden. Nicht vorhandene Parameter stehen innerhalb der Funktion nicht zur Verfügung, sondern sind undefined. Wenn zu viele Parameter übergeben werden, dann werden die überflüssigen Parameter ignoriert bzw. stehen im Bonusparameter (siehe unten!) zur Verfügung. function plus(x, y) { return x + y; } print(plus(1,2,3)); // 3 print(plus()); // NaN Alle übergebenen Parameter stehen neben den automatisch erzeugten Variablen zusätzlich im Bonusparameter, dem Array arguments, zur Verfügung. So lassen sich auch Funktionen mit einer beliebigen Anzahl von Parametern schreiben. 1. Reservierte Wörter wie null oder function sind keine gültigen JavaScript-Bezeichner. 4.3 Parameter und Parametervalidierung 37 function plusAll() { var result = 0; for (var i in arguments) { result += arguments[i]; } return result; } print(plusAll()); //0 print(plusAll(1,2,3,4,5)); // 15 Das Arguments-Array Achtung: Das Array arguments ist kein echtes Array. Es hat zwar ein length-Attribut und man kann über dieses iterieren, allerdings fehlen ihm die Methoden eines echten Array-Objekts. Da noch nicht einmal die Anzahl der übergebenen Parameter garantiert werden kann, ist Parametervalidierung in JavaScript beliebt. Je nach Anwendungsfall kann entschieden werden, ob die Funktion mit einer Exception beendet wird oder ob die Variable eines nicht übergebenen optionalen Parameters mit einem Defaultwert initialisiert wird. function plusWithHandling(x, y) { if (! x) { throw("x is undefined"); } y = y || 0; // 0 als Defaultwert return x + y; } try { print(plusWithHandling()); // Exception } catch (e) { print(e); // x is undefined } print(plusWithHandling(1)); // 1 print(plusWithHandling(0)); // 0 Oft findet man im Code (wie oben!) eine Prüfung, ob der Parameter den Wert true hat (bzw. zu true ausgewertet wird). if (! x) { throw("x is undefined"); } Dies ist aber dann gefährlich, wenn der Parameter beispielsweise der boolesche Wert false oder die Zahl -1 ist! 38 4 JavaScript als funktionale Programmiersprache Auch das Setzen des Defaultwerts auf 0 funktioniert hier nur, weil ein y-Parameter mit dem Wert 0 zu false ausgewertet wird und dann auf den gleichen Wert 0 gesetzt wird. y = y || 0; // 0 als Defaultwert Ob ein Parameter wirklich definiert ist, lässt sich über den typeof-Operator prüfen. Wenn der Parameter nicht definiert ist, so liefert der typeof-Operator undefined zurück. Besser ist es also, sowohl den Typ als auch den Wertebereich zu prüfen. function plusWithBetterHandling(x, y) { if (typeof x === "undefined" || x < 0) { throw("x is invalid"); } if (typeof y === "undefined" || y < 0) { y = 0; } return x + y; } print(plusWithBetterHandling(0, -1)); // 0 print(plusWithBetterHandling(0, "Meaning")); // 0; Der typeof-Operator bietet sich an, einen Parameter dahin gehend zu überprüfen, ob er überhaupt definiert ist und ob er vom richtigen Typ ist. Dies widerspricht aber den Paradigmen einer schwach typisierten Programmiersprache. function plusWithStrictHandling(x, y) { if (typeof x !== "number") { throw("x is of invalid type " + typeof x); } if (typeof y !== "number") { y = 0; } return x + y; } print(plusWithStrictHandling(new Number(-1),5)); //x is of invalid type object In diesem Beispiel wird überprüft, ob der übergebene Parameter x vom Typ number ist. Übergeben wird aber ein Number-Objekt vom Typ object. Daher wirft diese Implementierung eine Exception »x is of invalid type object«, obwohl die eigentliche Addition auch mit einem Number-Objekt funktioniert. Statt den Typ von Parametern zu überprüfen, sollte man also besser die Verwendung der Funktion mit Unit Tests testen. Die Testabdeckung sollte dabei allerdings den realistischen Anforderungen nachempfunden werden. Ein Entwickler weiß in der Regel, welche Parameter er einer Funktion übergibt, denn Typfehler treten in der Realität nur selten auf. 4.4 Konfigurationsobjekt als lesbarer Parameter 4.4 39 Konfigurationsobjekt als lesbarer Parameter Ein gebräuchliches Muster ist, einer Funktion nicht eine möglicherweise große Anzahl von Parametern, sondern ein Werte-Objekt zu übergeben. In einigen Kontexten wird dieses Werte-Objekt auch Konfigurationsobjekt oder Parameter-Bag genannt. In diesem Werte-Objekt befinden sich Parameter, die über einen Namen identifiziert werden. Dies führt zu einem lesbaren Funktionsaufruf, bei dem optionale Parameter einfacher weggelassen werden können. Diese optionalen Parameter lassen sich dann in der Funktion mit Defaultwerten initialisieren. function plusWithArgument(argument) { if (typeof argument !== "object") { throw("Argument not given"); } argument.operandA = argument.operandA || 0; argument.operandB = argument.operandB || 0; return argument.operandA + argument.operandB; } print(plusWithArgument({"operandA" : 1, "operandB" : 1})); //2 print(plusWithArgument({"operandB" : 1})); // 1 try { print(plusWithArgument()); } catch(e) { print(e); // Argument not given } 4.5 Anonyme Funktionen Wird der optionale Funktionsname bei der Definition einer Funktion weggelassen, dann handelt es sich um eine anonyme Funktion. Eine anonyme Funktion lässt sich – wie auch eine benannte Funktion – einer Variablen zuweisen. var anon = function () { print("anonymous"); } anon(); // anonymous Dass sich Funktionen Variablen zuweisen lassen, zeigt, dass auch Funktionen nicht anders als Objekte und Literale behandelt werden. Eine Funktion kann wie ein Objekt verwendet und (zur Laufzeit) verändert werden. Wenn eine benannte Funktion erzeugt wird, sollte im Code nach der Erzeugung kein Semikolon platziert werden. Dies ist eine Ausnahme in JavaScript, den ansonsten sollten stets Anweisungen durch ein Semikolon abgeschlossen werden. Wenn eine anonyme Funktion einer Variablen zugewiesen wird, dann sollte die Anweisung durchaus durch ein Semikolon abgeschlossen werden. Daher ist Code, der keine benannten Funktionen verwendet, sondern anonyme Funktionen, die einer Variablen zugewiesen werden, in sich konsistenter und lesbarer. 40 4 JavaScript als funktionale Programmiersprache Eine anonyme Funktion lässt sich direkt ausführen. Eine solche Funktion wird dann »Immediate Function« genannt. Dazu muss sie in runde Klammern eingeschlossen und direkt ausgeführt werden. print((function() { var x = 1; var y = 2; return x + y; })()); // 3 Selbstverständlich kann auch eine anonyme Funktion Parameter haben. print((function(x, y) { return x + y; })(1, 2)); // 3 Auch der Rückgabewert einer anonymen Funktion lässt sich einer Variablen zuweisen. var anonResult = (function () { return "anonymous"; })(); print(anonResult); // anonymous Genauso lässt sich der Rückgabewert einer anonymen Funktion selbst als Parameter einer Funktion übergeben. print(plus((function() { return 5; })(), 5) ); // 10 Anonyme Funktionen werden oft als Parameter verwendet. Genauso wie sich einer Funktion Werte als Parameter übergeben lassen, können einer Funktion auch Funktionen als Parameter übergeben werden. So ist eine Funktion nicht nur in Bezug auf ihre Daten, sondern auch in Bezug auf ihr Verhalten dynamisch. 4.6 Geltungsbereiche (Scopes) In JavaScript wird Code in Codeblöcken strukturiert. Anders als in anderen Cähnlichen Programmiersprachen kontrollieren diese Blöcke aber nicht die Sichtbarkeit von Variablen. In C-ähnlichen Programmiersprachen sind Variablen, die innerhalb eines Blocks definiert werden, nur in diesem Block sichtbar. In JavaScript hingegen sind alle Variablen, die innerhalb einer Funktion definiert werden – nicht nur ab dem Deklarationszeitpunkt – in der ganzen Funktion sichtbar. Deshalb spricht man von einem Funktionsgültigkeitsbereich. 4.7 this 41 function testScope() { if (true) { var a = "Ich bin sichtbar"; } print(a); } testScope(); // Ich bin sichtbar Interessant ist, dass eine Variable bereits verfügbar ist, bevor sie überhaupt deklariert wird. Dies macht folgendes Beispiel deutlich: var somevar = "globale Variable"; (function() { print(somevar); // undefined var somevar = "lokale Variable"; print(somevar); // lokale Variable }) ); Diese Funktion gibt in der ersten Print-Anweisung nicht etwa »globale Variable« aus, sondern »undefined«. Dies liegt daran, dass eine Funktion in zwei Stufen bearbeitet wird. Zunächst werden Variablen, Funktionsdeklarationen und Parameter erzeugt, in der zweiten Stufe wird der Code ausgeführt. Dieses Verhalten wird »hoisting« (heben) genannt. Da Variablen in einer Funktion immer schon zu Beginn der Funktion erzeugt werden, bietet es sich als guter Programmierstil an, diese auch bereits am Anfang einer Funktion zu deklarieren und mit Defaultwerten zu definieren. function declaresSomeVars() { var hello = "world", x = 5, y = 10; // here comes the code } 4.7 this Neben dem Bonusparameter arguments besitzt jede Funktion außerdem die Variable this. In einer objektorientierten Programmiersprache wie Java ist this meist das eigene Objekt. Dies ist in JavaScript oft, aber leider nicht immer der Fall. Die this-Variable wird erzeugt, wenn eine Funktion aufgerufen wird. Wenn eine Funktion als reine Funktion, d. h. nicht als Methode eines Objekts, aufgerufen wird, dann wird this mit dem globalen Objekt belegt. Das globale Objekt enthält – grob gesagt – alle globalen Variablen. Allerdings kann die this-Variable (z. B. bei Objekten) einen anderen Kontext haben. Dies ist eine nützliche Eigenschaft, denn so kann man eine Funktion in verschiedenen Kontexten wiederverwenden und hat trotzdem innerhalb der Funktion einen Bezug zum Kontext, in dem die Funktion als Methode aufgerufen wurde. Mehr zu Kontexten und Objeken findet man in den folgenden Kapiteln! 42 4 JavaScript als funktionale Programmiersprache Wenn man als Programmierer den Inhalt der Variablen this beim Aufruf einer Funktion bestimmen möchte, dann ist das über das Funktionsobjekt möglich. Funktionen in JavaScript sind vollwertige Objekte. Sie sind abgeleitet vom Prototyp function. Das Funktionsobjekt besitzt die Methoden call und apply. Sowohl in der apply-Methode als auch in der call-Methode lässt sich der Wert der this-Variablen übergeben. Alle weiteren Parameter, die der callMethode übergeben werden, entsprechen den Parametern der Funktion. So ist es möglich, eine Funktion in einem bestimmten Kontext, der durch die this-Variable festgelegt wird, aufzurufen. function returnThis() { return this; } print(returnThis()); // [object global] print(returnThis.call("Hello")); // Hello Der apply-Methode wird statt einer beliebigen Anzahl von Parametern das arguments-Array übergeben. 4.8 Geschachtelte Scopes und der globale Namensraum Jede Laufzeitumgebung hat einen globalen Namensraum. Dieser globale Namensraum ist ein globales Objekt. Das globale Objekt ist, wenn man sich nicht innerhalb einer Funktion befindet, das this-Objekt. Immer, wenn eine globale Variable deklariert wird, wird diese zu einer Eigenschaft des globalen Objekts. Variablen, die ohne die var-Anweisung deklariert werden, werden automatisch zu globalen Variablen. Das Problem am globalen Objekt ist, dass jede Funktion Zugriff auf dieses globale Objekt hat. Wenn eine Funktion beispielsweise eine Variable status im globalen Objekt definiert, eine andere Funktion – z. B. aus einer Bibliothek – später eine Variable mit gleichem Namen ebenfalls im globalen Objekt definiert, so überschreibt diese zweite Funktion die Variable der ersten Funktion. Das globale Objekt eignet sich also nicht, um Variablen zuverlässig zu speichern. Am besten wird dieser Zusammenhang von Nicholas C. Zakas in seinem Buch »High Performance JavaScript« dargestellt2: Funktionen sind Objekte und haben Attribute wie jedes andere Objekt. Eines dieser Attribute ist das interne Scope-Attribut. Das Scope-Attribut enthält eine Sammlung von Objekten, die den Scope repräsentieren, in dem die Funktion erzeugt wurde. Diese Sammlung wird die »scope chain« (Scope-Kette) genannt. Jedesmal wenn die Funktion ausgeführt wird, wird ein neuer interner Ausführungskontext erzeugt. Er repräsentiert den Kontext, in dem die Funktion aus2. Nicholas C. Zakas, »High Performance JavaScript«, O’Reilly, 2010. 4.8 Geschachtelte Scopes und der globale Namensraum 43 geführt wird. Dieser Ausführungskontext wird gelöscht, wenn die Funktion beendet wird. Der Ausführungskontext wird verwendet, um Namen (z. B. von Variablen) aufzulösen. Die Inhalte des Scope-Attributs, das bei der Definition der Funktion erzeugt wurde, wird in den Ausführungskontext kopiert. Sobald dies beendet ist, wird ein Activation-Objekt erzeugt, das ebenfalls in den Ausführungskontext kopiert wird, und zwar an die erste Stelle. Das Activation-Objekt enthält alle lokalen Variablen, den Bonusparameter arguments und das this-Objekt. Wenn ein Name aufgelöst werden soll, dann werden alle Objekte, die in der Scope-Kette liegen, durchsucht. Es wird z. B. zuerst das Activation-Objekt, dann das globale Objekt durchsucht. Wenn eine Funktion innerhalb einer anderen Funktion erzeugt wird, dann enthält die Scope-Kette des inneren Objekts ein Scope-Objekt zwischen dem Activation-Objekt und dem globalen Objekt, das dem Activation-Objekt der äußeren Funktion entspricht. Bei der Auflösung eines Namens wird also zuerst das Activation-Objekt der inneren Funktion, dann das Activation-Objekt der äußeren Funktion und zum Schluss das globale Objekt durchsucht. Vereinfacht ausgedrückt bedeutet dies, dass ein Name von innen nach außen aufgelöst wird. Es wird von innen nach außen so lange nach dem Namen gesucht, bis er gefunden wird (je tiefer die Schachtelung der Scopes, desto länger dauert es, einen Namen aufzulösen – die Dauer der Namensauflösung lässt sich jedoch in der Regel vernachlässigen). 44 4 JavaScript als funktionale Programmiersprache Für das Activation-Objekt der äußeren Funktion bedeutet dies aber, dass das Activation-Objekt nicht dann schon vom Garbage Collector gelöscht werden kann, wenn die äußere Funktion beendet wurde, sondern erst dann, wenn auch die innere Funktion beendet ist, da die innere Funktion noch eine Referenz auf das Activation-Objekt der äußeren Funktion hält. Dies kann zu Speicherproblemen wie Memory-Leaks führen. 4.9 Closures und Module Funktionen beinhalten Code. Eine Funktion wird stets in einem Funktionsgeltungsbereich ausgeführt. In der theoretischen Fachliteratur wird eine Kombination aus Code und Geltungsbereich als Closure bezeichnet. Der Funktionsgeltungsbereich gilt auch, wenn in einer Funktion eine weitere, innere Funktion definiert wird. Diese Funktion hat dann Zugriff auf die Variablen der äußeren Funktion. Ausnahmen sind hier this und der Bonusparameter arguments, da diese Parameter von jeder Funktion selbst überschrieben werden, damit in der Scope-Kette die eigene this-Variable und der Bonusparameter arguments vor denen der äußeren Funktion gefunden werden. Soll die this-Variable der äußeren Funktion der inneren Funktion zur Verfügung gestellt werden, so ist es üblich, in der äußeren Funktion eine that-Variable zu definieren und dieser die this-Variable zuzuweisen. Dann kann die innere Funktion über die that-Variable auf die this-Variable der äußeren Funktion zugreifen. Ein anderer üblicher Name für die that-Variable ist self. Dies ist aber verwirrend, da self in manchen Sprachen wie Smalltalk eigentlich der this-Variablen entspricht. Hier ein Beispiel für eine Closure. function outerFunction() { var x = "Hello"; function innerFunction() { return x; } return innerFunction(); } print(outherFunction()); // Hello Die innere Funktion innerFunction hat Zugriff auf die Variable x der äußeren Funktion outerFunction. Dieses Verhalten ist auf den ersten Blick nicht weiter überraschend. Interessant werden Closures, wenn eine innere Funktion zurückgegeben wird. Diese innere Funktion hat nämlich Zugriff auf die Variablen der Funktion, in der sie definiert wurde. Diese Besonderheit wird in der Praxis als Closure bezeichnet.3 Closures lassen sich verwenden, um private Variablen zu deklarieren. Man spricht dann von Modulen. 3. Auch wenn in der Theorie alle ausgeführten JavaScript-Funktionen Closures sind. 4.10 Funktionale Vererbung 45 JavaScript kennt eigentlich keine privaten Variablen, aber es kennt Geltungsbereiche. Die folgende anonyme Funktion hat eine Variable variable. Auf diese Variable hat die innere Funktion inc Zugriff. Diese innere Funktion wird als Rückgabewert der Variablen incrementor zugewiesen. incrementor hat nun Zugriff auf die Funktion inc, nicht jedoch auf die Variable variable. Diese Variable erscheint nun wie eine private Variable. var incrementor = function () { var variable = 0; return { inc : function() { return variable ++; } }; }(); print(incrementor.inc()); // 0 print(incrementor.inc()); // 1 print(incrementor.variable); // undefined Wichtig zu verstehen ist, dass die innere Funktion wirklich Zugriff auf die Variablen der äußeren Funktion – das Activation-Objekt – hat und nicht nur auf eine Kopie. Wenn sich die Variable der äußeren Funktion verändert oder von der inneren Funktion verändert wird, so erfolgt der Zugriff zukünftig auf die geänderte Variable. Es lassen sich mit Closures also keine Zustände konservieren. 4.10 Funktionale Vererbung Funktionale Vererbung ist ein Begriff, der durch Douglas Crockford geprägt wurde. Funktionen sind Objekte. Wie Objekte können Funktionen auch selbst Eigenschaften (Properties) und Methoden (Funktionen) besitzen. Funktionale Vererbung implementiert man, indem man eine Funktion erstellt, die Objekte erzeugt und diese zurückgibt. Innerhalb dieser objekterzeugenden Funktion gibt es private Instanzvariablen. Auf diese kann das zurückgegebene Objekt als Closure zugreifen. Für die Nutzer des zurückgegebenen Objekts sind diese jedoch unsichtbar. Das zurückzugebende Objekt wird um Methoden erweitert. Diese Methoden haben ebenfalls Zugriff auf die privaten Variablen. function square(width) { var squareObject = {}; squareObject.getArea = function() { if (width > 0) { return width * width; } } return squareObject; } 46 4 JavaScript als funktionale Programmiersprache var mySquare = square(5); print(mySquare.getArea()); // 25 print(mySquare.width); // undefined Vererbung lässt sich in der funktionalen Vererbung durch das Dekorierer-Entwurfsmuster implementieren. Um einen Subtyp zu implementieren, erweitert man den Supertyp um die benötigten Eigenschaften oder Methoden. Zu überschreibende Methoden speichert man in privaten Variablen, um diese auch dann noch verwenden zu können, wenn man die Methoden des Super-Objekts bereits überschrieben hat. function box(width) { var boxObject = square(width); var getSquareArea = boxObject.getArea; boxObject.getVolume = function() { if (width > 0) { return getSquareArea() * width; } } boxObject.getArea = function() { if (width > 0) { return getSquareArea() * 6; } } return boxObject; } var myBox = box(5); print(myBox.getVolume()); // 125 print(myBox.getArea()); // 150 Allerdings hat funktionale Vererbung einige Nachteile. Jedes Mal wenn ein neues Sub-Objekt erzeugt wird, werden zwei neue Funktionen erzeugt, einmal für das Sub-Objekt und einmal für das Super-Objekt. Diese Objekte sind nicht leichtgewichtig, denn das innere Super-Objekt wird als Closure im äußeren Sub-Objekt referenziert. Das Verwenden von vielen Closures kann zu Memory-Leaks führen. Typen können nicht durch den instanceof-Operator getestet werden. Sowohl das Square-Objekt als auch das Box-Objekt sind einfache Funktionen. Ein BoxObjekt ist keine Instanz des Square-Objekts. Dies liegt daran, dass keine Konstruktor-Funktion verwendet wird. print(myBox instanceof square); // false Typen lassen sich zudem nicht erweitern. Das Super-Objekt wird in einer Closure gehalten. Es ist von außen nicht sichtbar. Die Syntax zum Erzeugen einer neuen Objektinstanz entspricht nicht den Konventionen von JavaScript. var myBox = box(5); 4.11 Callbacks 47 Neue Objektinstanzen werden in JavaScript mit dem new-Operator auf einer Konstruktor-Funktion erzeugt. Da das Muster der funktionalen Vererbung keine Konstruktor-Funktionen kennt, kann der new-Operator nicht verwendet werden. Dies kann zu Fehlern in der Verwendung des Codes führen. print(new Box(5)); // ReferenceError: Box is not defined Funktionale Vererbung sollte also nur in Ausnahmefällen angewendet werden, in denen ein funktionales Paradigma am sinnvollsten erscheint. Mehr zu Konstruktoren und Konstruktor-Funktionen wird im Kapitel über objektorientierte Programmierung beschrieben. 4.11 Callbacks Erst Funktionen ermöglichen die effiziente Verarbeitung von asynchronen Programmabläufen. Fast jedes Programm lässt sich synchron schreiben. Angenommen, ein Programm lädt Daten über ein Netzwerk. Nachdem es diese Daten geladen hat, verarbeitet es diese Daten. Dies ließe sich wie folgt lösen. var xhReq = new XMLHttpRequest(); xhReq.open("GET", "someURL", false); xhReq.send(null); var daten = xhReq.responseText; print(daten); Dieser Ansatz hat einen entscheidenden Nachteil. Das Programm wird blockiert, bis der Request abgearbeitet ist. Da JavaScript eine Sprache ist, die per Definition nur in einem Thread läuft, ist so lange der ganze Prozess oder Thread blockiert und benötigt Systemressourcen. Daher ist dieser blockierende Ansatz nicht zu empfehlen. Besser ist der Ansatz, dies über Callbacks zu lösen. Dazu muss dem Request eine Callback-Funktion zur Verfügung gestellt werden. Funktionen sind Objekte. Darum können sie einer Funktion als Parameter übergeben werden. Diese übergebene Callback-Funktion wird dann aufgerufen, wenn der Request beendet ist und die Daten vorliegen. Der Request wartet nicht, bis die Response vorliegt, sondern kehrt sofort zurück. request.on('response', function (response) { response.on('data', function (daten) { print(daten); }); }); Diese Art der asynchronen Requests stellen Node (node.js) auf dem Server und Ajax-Frameworks im Webbrowser zur Verfügung. Node.js wird in Kapitel 10 besprochen. 48 4 JavaScript als funktionale Programmiersprache Doch wie programmiert man selbst eine Funktion mit Callbacks. Als Beispiel schreiben wir eine Funktion, die eine lange Berechnung durchführt – die Summe aller Zahlen von eins bis eintausend. Diese Funktion wird zuerst synchron programmiert und anschließend zu einer asynchronen Callback-Funktion überarbeitet. function calculate1To1000Synchronous() { var result = 0; for (var i = 1; i <= 1000; i++) { result += i; } return result; } var summe = calculate1To1000Synchronous(); print(summe); // 500500 Da in Wirklichkeit kein I/O durchgeführt wird, läuft diese Funktion nach dem Refactoring natürlich immer noch in einem einzigen Thread. Exemplarisch wird hier allerdings nun eine anonyme Callback-Funktion übergeben und nach der Berechnung des Werts aufgerufen. function calculate1To1000WithCallback(callback) { var result = 0; for (var i = 1; i <= 1000; i++) { result += i; } callback(result); } calculate1To1000WithCallback(function(data) {print(data)}); // 500500 4.12 Kaskaden Viele Funktionen haben keinen Rückgabewert, sie liefern undefined zurück. Dies betrifft vor allem Setter. Wenn man diese Funktionen (eigentlich: Methoden) this statt undefined zurückliefern lässt, dann lassen sich Kaskaden von Funktionsaufrufen schreiben. Dies wird hier am fiktiven Beispiel einer Turtle-Graphic-Implementierung in JavaScript demonstriert: move(100).rotate(90).move(200).rotate(30).move(20).penUp().move(20).penDown(); Kaskaden eignen sich, um komplexe Funktionsaufrufe mit vielen Parametern zu einfachen, lesbaren Funktionsaufrufen mit wenigen Parametern zu refaktorisieren. 4.13 Rekursion 49 4.13 Rekursion Viele Probleme lassen sich elegant durch Rekursion lösen. Eine rekursive Funktion ist eine Funktion, die sich selbst aufruft. Bei jedem Aufruf der Funktion werden Werte auf den Stack gelegt. Rekursive Funktionen mit einer hohen Rekursionstiefe können daher zu einem Stack-Overflow führen. Dies sollte dem Leser bereits bekannt sein. Das folgende Beispiel berechnet die Fakultät der übergebenen Zahl. Die Fakultät einer Zahl wird sehr schnell sehr groß. Daher passt die Fakultät von 256 nicht in eine JavaScript-Zahl. Bei einer zu hohen Rekursionstiefe bricht die Funktion mit einem RangeError ab. function factorial(n) { if (n <= 1) { return 1; } else { return n * factorial(n-1); } } print("Fakultät von print("Fakultät von print("Die Fakultät factorial(100000)); 4: " + factorial(4)); //24 256 ist zu groß: " + factorial(256)); // Infinity von 100000 lässt sich nicht berechnen: " + // RangeError: Maximum call stack size exceeded Der berechnete Wert ist größer als der zur Verfügung stehende Wertebereich, also wird Infinity als Ergebnis geliefert. 4.14 Funktions-Caching Angenommen, eine Funktion führt eine komplexe Berechnung aus. Diese Berechnung ist nur abhängig von den übergebenen Parametern und nicht vom Kontext, in dem diese Funktion ausgeführt wird. Dann lässt sich der Rückgabewert der Funktion cachen. Dieses Entwurfsmuster wird »Memorization« genannt. Memorization wird hier wieder an der Fakultätsfunktion dargestellt, da diese sich besonders für das Caching anbietet. function cachedFactorial(n) { if (n <= 1) { return 1; } else if (!cachedFactorial.cache[n]) { var result = n * cachedFactorial(n-1); cachedFactorial.cache[n] = result; } return cachedFactorial.cache[n]; } cachedFactorial.cache = {}; 50 4 JavaScript als funktionale Programmiersprache print(cachedFactorial(5)); // 120, 5 Rekursionen und befüllen des Cache print(cachedFactorial(6)); // 720, nur eine Rekursion da 2..5 bereits im Cache im sind 4.15 Currying Der Begriff Currying hat nichts mit der Gewürzmischung zu tun, sondern geht zurück auf den Mathematiker Haskell Curry, nach dem die Programmiersprache Haskell benannt ist. Durch Currying lassen sich Parameter einer Funktion vorbefüllen. Die thisVariable bleibt vom Currying unberührt. Currying funktioniert, indem man eine neue Funktion aus einer bestehenden Funktion und einem vorzubefüllenden Parameter zusammensetzt. Dazu muss eine Curry-Methode üblicherweise im Prototyp des Funktionsobjekts implementiert werden. An dieser Stelle folgt die Implementierung von Douglas Crockford, da diese sicherlich die verbreitetste Currying-Implementierung ist. Allerdings musste sie angepasst werden, da die ursprüngliche Beispielimplementierung nicht in allen Umgebungen lauffähig ist. Function.prototype.curry = function() { var slice = Array.prototype.slice, // get the slice method args = slice.apply(arguments), // preserve ags array for closure that = this; // preserve this for closure return function() { return that.apply(null, args.concat(slice.apply(arguments))); }; }; Diese Currying-Funktion lässt sich nun einsetzen. Wir verwenden die plus-Funktion, die eigentlich zwei Parameter erwartet, und gehen davon aus, dass in unserem Anwendungsfall stets zu einem Basiswert hinzuaddiert werden soll. var addToBase = plus.curry(100); print(addToBase(5)); // 105 4.16 Zusammenfassung In diesem Kapitel haben Sie JavaScript als funktionale Programmiersprache kennengelernt. Sowohl die Deklaration als auch die Verwendung von Funktionen sollten bekannt sein. Scopes sollten verstanden worden sein, denn Scopes sind im Zusammenhang mit Closures wichtig. JavaScript ist jedoch nicht nur eine funktionale, sondern auch eine prototypische Programmiersprache. Daher wird im folgenden Kapitel JavaScript als prototypische Programmiersprache behandelt.