4 JavaScript als funktionale Programmiersprache

Werbung
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.
Herunterladen