Funktionale Erweiterung von JavaScript

Erweitere problemlos dein JavaScript

JavaScript ist eine objektbasierte Skriptprache mit dynamischer Typisierung, mit deren Hilfe man Webseiten mit clientseitigem “Verhalten” versehen kann. Im Zeiten von Web 2.0 und insbesondere Ajax kann man beobachten, dass Seiten zunehmend dynamischer werden. Ihre Programmlogik und die damit verbundene Entwicklungsarbeit werden immer umfangreicher. Es stellt sich daher die Frage, inwieweit die Sprache für solche Anforderungen geeignet ist.

An dieser Stelle möchte ich nun einige bekannte Eigenschaften von JavaScript betrachten. Im Zusammenhang mit der Eignung für umfangreiche Entwicklungen sind zwei Eigenschaften von JavaScript interessant. Diese verleihen Programmen einen – mitunter überraschend – hohen Grad an Dynamik.

Zunächst ist die Sprache objektbasiert im folgenden Sinne. Vererbungsbeziehungen werden über eine sogenannte prototypische Vererbung ad-hoc aufgebaut. Das bedeutet nichts anderes, als dass jedes Objekt zu jeder Zeit um beliebige Eigenschaften und Methoden erweitert werden kann. Weiterhin gibt es keine mit den “klassischen” objektorientierten Sprachen vergleichbaren sprachlichen Mittel, um Daten zu kapseln. Es gibt zwar Wege, eine Kapselung über lokale Variablen in Konstruktorfunktionen zu erreichen, allerdings gibt es hierfür keine explizite Ausdrucksweise wie etwa in C++ oder Java.

Zum anderen nimmt der Interpreter zu keiner Zeit eine Prüfung von Funktionsaufrufen vor. Weder Typ noch die Anzahl (!) der übergebenen Parameter werden mit der Definition einer Funktion verglichen.

Beispiel:

Eine Funktion

[code lang="JavaScript"]
function test (p_1, p_2, p_3) {
alert (p_1); alert (p_2); alert (p_3);
}
[/code]

kann als test (1), test (1,2) oder test (1,2,3) aufgerufen werden. Nicht übergebene Parameter haben innerhalb der Funktion den Wert “undefined”. Diese und andere Eigenschaften führen dazu, dass es bei umfangreichen Programmteilen mitunter schwierig ist, abzuschätzen, wie sich ein Stück Code bei seinem Aufruf verhält.

JavaScript ist natürlich nicht ohne Grund so geraten. Vielmehr eignen sich dynamische Skriptsprachen gut zur Entwicklung von Prototypen oder einfacher Programmlogiken. Bei umfangreichen Entwicklungen ist – zumindest meiner Erfahrung nach – ein “ordendliches”, sprich: statisches Typsystem durchaus eine große Hilfe. Die Dinge sind nun aber wie sie sind und kann man versuchen, das Beste daraus zu machen. In diesem Artikel möchte ich daher zeigen, wie man JavaScript etwas von dem Feeling einer funktional Sprache verleihen kann. Funktionale Sprachen zeichnen sich in gewissem Sinne dadurch aus, dass Programme auf einer abstrakteren Ebene formuliert und dadurch kompakter werden. Davon verspreche ich mir zweierlei: Zum einen wird die Lesbarkeit des Codes verbessert, zum anderen werden Wiederverwendbarkeit und Robustheit erhöht, da für häufig verwendete Konzepte benannte Strukturen definiert werden.

Funktionale Programmiersprachen zeichnen sich im wesentlich durch zwei Konzepte aus:

  1. Referentielle Transparenz
  2. Funktionen sind First-Class-Objects

Referentielle Transparenz bedeutet, dass der Wert einer Variablen nur von ihrer Umgebung abhängt und nicht vom Zeitpunkt ihrer Verwendung. Variablen werden daher eher “im mathematischen Sinn” aufgefasst und weniger als benannte Speicherplätze. Insbesondere ist eine Variable, für die referentielle Transparenz gilt, unabhängig von Seiteneffekten. JavaScript bietet dieses Konzept nicht. Bei Funktionsaufrufen kann man ihm aber durch Wertkopien und die ausschließliche Verwendung lokaler Variablen nahekommen.

Werden Funktionen als First-Class-Objects aufgefasst, können sie genauso wie Werte behandelt werden und damit auch Rückgabewerte oder Parameter von Funktionen sein oder zur Laufzeit erzeugt werden. Das geht über das Konzept eines einfachen Funktionszeigers in C hinaus – hier sind bspw. keine anonymen oder partiell evaluierten Funktionen möglich. In JavaScript sind Funktionen Objekte. Sie können zur Laufzeit – auch anonym – erzeugt und verarbeitet werden. Darüber hinaus sind Funktionen in JavaScript Closures. Das bedeutet, eine lokal definierte Funktion “erbt” das sie umgebende lexikalische Scope.

[code lang="JavaScript"]
function testClosure () {
var myInt = 1;
var closure = function (e) {
return e + myInt;
}
alert (closure ( 2));
}

[/code]

Innerhalb von closure () ist myInt sichtbar, obwohl es nicht global deklariert ist. Mit Hilfe dieser Eigenschaft ist es bspw. auch möglich, Eventhandler zu parametrieren, ohne globale Variablen zu verwenden.

JavaScript bietet mit diesen Eigenschaften nun die Möglichkeit, sogenannte Funktionale zu definieren. Das sind im syntaktischen Sinne gewöhnliche Funktionen mit der Besonderheit, dass sie eine oder mehrere Funktionen als Parameter entgegen nehmen und deren Ausführung steuern. Ich möchte hier exemplarisch drei Beispiele für Funktionale zeigen, die auch in anderen Sprachen verfügbar sind. Diese sind map, filter und foldr.

Map nimmt eine Liste l und eine unäre Funktion f als Parameter und wendet f auf jedes Element von l an. Die Parameter von Filter sind eine Liste l und ein unäres Prädikat p. Die Rückgabe von filter sind dann alle Element von l, für die p gilt. Foldr ist etwas komplizierter. Die Parameter sind eine Liste l, eine binäre Funktion f und ein für die Funktion neutrales Element n. Die Rückgabe von foldr ist ein einziges Element, das folgendermaßen bestimmt wird: f wird auf das letzte Element von l und n angewendet. Das Ergebnis nennen wir e1 := f (l[-1], n). Dann wird f auf e1 und das vorletzte Element von l angewendet: e2 := f (l[-2], e1), u.s.w. bis nur noch ein Element übrigbleibt. Mit Hilfe von foldr kann man Elemente kumulieren, Maxima ermitteln und Listen sortieren – vorausgesetzt man definiert eine entsprechende Funktion.

Im folgenden zeige ich, wie diese Funktionale in JavaScript realisiert werden können. Durch sein Konzept der prototypischen Vererbung bietet JavaScript die Möglichkeit, vorhandene “Klassen” auch nachträglich um eigene Methoden zu erweitern. Unsere Funktionale arbeiten auf Listen – es bietet sich daher an, sie dem eingebauten “Listen”-typ – dem Array als Methoden in die Schuhe zu schieben. Die vorgestellten Implementierungen werden daher als Erweiterungen des Array-Prototyps realisiert. Um eine Art referentielle Transparenz zu erhalten, legen alle Methoden Kopien der übergebenen Felder an.

Eine Implementierung von map () lässt sich in JavaScript folgendermaßen realisieren:

  1. p_Func ist die Funktion, die auf jedes Element der Array-Instanz angewendet wird

[code lang="JavaScript"]
Array.prototype.map = function (p_Func) {
var ret = new Array (this.length);
for (var i = 0; i < this.length; i++) {
ret[i] = p_Func (this[i], i);
}
return ret;
};

[/code]

Im Fall von Filter kann das Ergebnis dann so aussehen:

  1. p_Func ist das Prädikat, das auf das Array angewendet werden soll
  2. p_bReturnIndex gibt an, ob das Ergebnis-Array die Indices oder die Elemente beinhalten soll
  3. p_bPreserveIndex gibt an, ob nicht enthaltene Elemente ausgespart werden sollen
  4. p_Neutral ist ein neutraler Wert, der für nicht enthaltene Werte gesetzt wird, sofern p_bPreserveIndex gesetzt ist

[code lang="javascript"]
Array.prototype.filter = function (p_Func, p_bReturnIndex, p_bPreserveIndex, p_Neutral) {
var ret = new Array ();
for (var i = 0; i < this.length; i++) {
if (p_Func (this[i], i)) {
if (p_bPreserveIndex) {
(p_bReturnIndex) ? ret[i] = i : ret[i] = this[i];
} else {
(p_bReturnIndex) ? ret.push (i) : ret.push (this[i]);
}
} else if (p_bPreserveIndex) {
(p_bReturnIndex) ? ret[i] = -1 : ret[i] = p_Neutral;
}
}
return ret;
};
[/code]

Map kann auf die folgende Weise umgesetzt werden:

  1. p_Func ist die Funktion, die auf jedes Element angewendet werden soll. An dieser Stelle wird eine binäre Funktion übergeben, die als zweites Argument den aktuellen Index des Elements erhält.

[code lang="javascript"]
Array.prototype.map = function (p_Func) {
var ret = new Array (this.length);
for (var i = 0; i < this.length; i++) {
ret[i] = p_Func (this[i], i);
}
return ret;
};
[/code]

Die Umsetzung von Foldr ist wie folgt:

  1. p_Func ist eine binäre Funktion
  2. p_Neutral ist ein neutrales Element dieser Funktion

[code lang="javascript"]
Array.prototype.foldr = function(p_Func, p_Neutral) {
return (this.length == 0) ? p_Neutral :
p_Func(this[0], this.slice(1).foldr(p_Func, p_Neutral));
};
[/code]

Die Funktionalität gegenüber einer “einfachen” Schleife ist hier nicht eingeschränkt. Lokal definierte Funktionen in JavaScript sind sogenannte Closures. Das bedeutet, dass Variablen aus dem umgebenden Scope im inneren einer solchen Funktion sichtbar sind.

Beispiel:

[code lang="javascript"]
var myArray = new Array (1,2,3,4);
var myInt = 3;
var smaller = myArray.filter (function (p_El) { return p_El < myInt; } );
[/code]

Die Variable myInt ist innerhalb der anonymen Funktion sichtbar, obwohl sie außerhalb definiert wurde.

Was erreichen wir damit? Zum einen erhält man eine bessere Wiedererkennbarkeit von Codeabschnitten. In einer Schleife kann im Prinzip alles mögliche getan werden – bei einem Funktional ist auf einen Blick klar, was seine Aufgabe ist. Zum anderen bieten zwingt man sich letztendlich auch dazu, seinen Code besser durch Funktionen zu strukturieren. Neben anonymen Funktionen können die Funktionale auch benannte Funktionen mit einer passenden Signatur entgegen nehmen. Das trägt widerum zu einer besseren Lesbarkeit und darüber hinaus zu einer höheren Wiederverwendbarkeit verglichen mit einem einfachen Schleifenrumpf bei.

Beispiel:

In dem Array myArray sollen alle geraden Zahlen mal 2 genommen werden.

[code lang="javascript"]
var myArray = new Array (1,2,3,4);
[/code]

In einer einfachen Schleife sieht das etwa so aus:

[code lang="javascript"]
var newArray = new Array ();
for (var i = 0; i < myArray.length; i++) {
if (myArray[i] % 2 == 0) {
newArray.push (2 * myArray[i]);
}
}
[/code]

Der funktionale Ausdruck ist hingegen deutlich kompakter als die Schleife:

[code lang="javascript"]
var twotime = function (p_Par) { return p_Par * 2; }
var even = function (p_Par) { return p_Par % 2 == 0; }
var newArray = myArray.map (twotime).filter (even);
[/code]

Der auf diese Art veränderte Array-Prototyp wirkt sich auf alle Instanzen aus, die im Anschluss an die Deklarationen gebildet werden. Insbesondere bei der Arbeit mit DOM-Objekten muss man allerdings beachten, dass diese bereits im Browser instatiiert sind, bevor externe JavaScript-Quellen geladen werden. Um also Arrays, wie sie von DOM-Objekten geliefert werden, auf diese Art verwenden zu können, muss man eine neue Array-Instanz bilden. Das kann man am einfachsten mit einer Art Konstruktor-Funktion erreichen:

[code lang="JavaScript"]
Array.prototype.construct = function (a) {
for (var i = 0; i < a.length; i++) {
this.push (a[i]);
}
return this;
};
[/code]

Gehen wir nun davon aus, dass wir es mit einer Html-Tabelle zu tun haben, die in jeder ihrer Zellen eine Textbox beinhaltet. Der Einfachheit halber sollen zunächst lediglich die Werte dieser Textboxen beschafft werden. Mit Hilfe von Funktionalen kann diese Aufgabe lösen.

Wir brauchen zunächst eine Funktion, welche die Textboxen ermittelt.

[code lang="JavaScript"]
var fnGetTextbox = function (nd, pos) {
return nd.getElementsByTagName("input")[0];
};
[/code]

[code lang="JavaScript"]
function getTextboxes (p_Table) {
var vndCells = new Array ().construct (p_Table.getElementsByTagName ("td"));
var vtxtInput = vndCells.map (fnGetTextbox);
return vtxtInput;
}
[/code]

Weiterhin definieren wir eine Funktion, die den Wert einer Textbox liest.

[code lang="JavaScript"]
var fnGetValue = function (nd, pos) {
return (nd ? nd.value : '');
};

[code lang="JavaScript"]
var vtxtInput = getTextboxes (row);
var vValues = vtxtInput.map (fnGetValue);
[/code]

Um aufwändigere Aufgaben zu erledigen, kann man map () mit einer lokalen Funktion parametrieren.

[code lang="JavaScript"]
var vtxtInput = getTextboxes (row);
var newValue = "Inhalt: ";
var vValues = vtxtInput.map (function (p_Nd, p_iPos) {
p_Nd.value = newValue + p_iPos;
return p_Nd;
});
[/code]

Selbstverständlich sind Funktionale nicht wirklich mächtiger als Schleifen. Der entscheidende Unterschied, der nicht zuletzt beim Einsatz mit DOM zum Tragen kommt, ist allerdings der folgende: Das Wissen um die Strukturen innerhalb der Listenelemente (sprich: wie liegen Eigenschaften von Elementen innerhalb von Zellen einer Tabelle) ist innerhalb einer Funktion gekapselt. Auf diese Weise wird Code, der für die Traversierung einer Struktur verantwortlich ist, getrennt von der Logik, die einzelne Elemente betrifft.

 

 

 

    Folgende Kommentare wurden auf unserem alten Blog zu diesem Beitrag veröffentlicht:

    Sepp123 schreibt:November 24th, 2007 at 1:27

       

      hm… tolle idee – erinnert ein bisschen an Lisp :-) aber wie sieht das denn leistungsmäßig aus? Durch die Kopiererei werden doch laufend neue Objekte erzeugt.

      Gruss vom Sepp

    szimmer schreibt: November 24th, 2007 at 6:27

       

      Das kann man pauschal schwer beantworten. Natürlich ist eine Lösung mit Schleifen performanter. Meiner Erfahrung nach geht die Allokation von Arrays in JavaScript allerdings ausreichend schnell von Statten. Selbst bei Seiten mit einigen Hundert Tabellenzellen habe ich keine spürbaren Verzögerungen erlebt.
      Wie sich das in Zahlen ausdrückt, habe ich noch nicht ermittelt. Aber wenn jemand neugierig genug ist, es auszumessen, bin ich natürlich auch an den Ergebnissen interessiert :-)