Zusammenfassung: Dieser Text handelt von Problemen der Programmiersprache JavaScript. Dabei werden sowohl Entwurfsfehler erläutert als auch solche, welche eher konzeptueller Natur sind. Es wird jeweils darauf eingegangen, wie eine Lösung ausschauen kann oder hätte ausschauen können.
Normalerweise sollte die Umwandlung eines Arrays in eine Zeichenkette auch genau so ausschauen wie das Literal. Aber in JavaScript fehlen die eckigen Klammern:
> String([1, 2]) "1,2"
Man kann sich denken: »Gut, die kann ich ja noch hinzufügen«. Aber bei verschachtelten Arrays wird es schlimmer:
> String([[1, 2], [3, 4]]) > "1,2,3,4"
Außerdem kommt hinter einem Komma üblicherweise ein Leerzeichen. Python macht es richtig.
> [1, 2] + 3 "1,23"
Was passiert hier? Nun, das Array wird vor der Addition implizit in eine Zeichenkette umgewandelt. Wie wir schon gesehen haben, wird bei der Umwandlung eines Arrays in eine Zeichenkette die Klammerung ausgelassen.
Man wird es kaum glauben: JavaScript ist typsicher. Ist ein JavaScript-Interpreter korrekt implementiert, so kann kein JavaScript-Programm die Speichersicherheit gefärden. Nur weil JavaScript typsicher ist, heißt das aber nicht, dass es auch streng typisiert ist.
Einige Operationen sind in JavaScript nach Regeln definiert, welche keinen praktischen Sinn ergeben.
> [] == [] false > [1, 2] == [1, 2] false > [1] == [1] false
Was soll das?
> [1] == 1 true
Ja. Logisch, oder?
> [] == 0 true > [] == 1 false
Ja.
Die Funktion Array(n)
ist nicht zu gebrauchen.
Felder haben offenbar eine tieferliegende Komplexität, gegen
die der Programmierer ankämpfen muss.
> [undefined, undefined, undefined, undefined].map(x => 1) [1, 1, 1, 1] > Array(4).map(x => 1) Array(4) [undefined, undefined, undefined, undefined] > a = Array(4); > a[0] = undefined; > a.map(x => 1) Array(4) [1, undefined, undefined, undefined]
In JavaScript ist es vorgesehen, dass eine undeklarierte Variable auf der linken Seite einer Zuweisenung automatisch global ist. In der Funktion
function f(x){ y = 2*x; return y; }
wird z. B. unintuitiverweise die globale Variable
y
geändert, was man höchstwahrscheinlich nicht wollte.
Ein solches Verhalten führt in einer Vielzahl von Fällen zu seltsamen
Bugs. Ein explizite deklaration von globalen Variablen wäre sinnvoll:
function f(x){
global y = 2*x;
return y;
}
In JavaScript ist Operatorüberladung leider nicht möglich, obwohl es trivial zu implementieren wäre. Es kommt stattdessen folgends Verhalten zustande:
> x = {}; y = {} > x + y "[object Object][object Object]"
Der Interpreter überprüft ja schon zur Laufzeit, welchen Datentyp
die Variablen x
, y
besitzen. Falls der
Datentyp kein einfacher ist, werden die Objekte in diesem Residualfall
einfach implizit in Zeichenketten umgewandelt. Genau in diesem Residualfall
wäre es aber möglich gewesen, eine zugehörige Methode add
im Objekt x
oder in dessen Prototypenkette aufzurufen:
x + y ≡≡ x.add(y)
In JavaScript lässt sich unter Zuhilfenahme von Closures der Kompositionsoperator formulieren:
function compose(g, f){ return function(x){return g(f(x));}; }
Diesen Operator möchten wir nun als Methode einer Funktion haben. Man macht folgenden Ansatz:
Function.prototype.o = function(f){ return function(x){return this(f(x));}; }
Hier ergibt sich das Problem, dass das this
-Argument
zur inneren Funktion gehört, und nicht zur äußeren.
Stattdessen ist man gezwungen zu:
Function.prototype.o = function(f){ var g = this; return function(x){return g(f(x));}; }
Es wäre sinnvoll, wenn es in JavaScript eine Möglichkeit geben
würde, den Namen des this
-Arguments individuell
festzulegen, wie bei jeder anderen gewöhnlichen Variable auch. Z.B. so:
Function.prototype.o = function(g; f){ return function(x){return g(f(x));}; }
In JavaScript koinzidieren Wörterbücher mit strukturierten Objekten. Die Syntax von Wörterbuch- bzw. Objekt-Literalen besitzt das Manko, dass zunächst keine Ausdrücke als Schlüssel möglich sind. Weil nämlich die Schreibweisen
t = {"a": 1, "b": 2}; t = {a: 1, b: 2};
äquivalent sind, wäre die letztere Schreibweise zweideutig, wenn Ausdrücke als Schlüssel auf der linken Seite zugelassen wären. Man hat diesen Umstand in ES6 durch die Notation
t = {["a"]: 1, ["b"]: 2};
umgangen. Diese Notation ist umständlich lang und schaut dahingehend seltsam aus, dass die Schlüssel doch eigentlich auch Arrays sein könnten:
t = {[[1, 2]]: 1, [[3, 4]]: 2};
Hier ergibt sich ein weiteres Problem: alle Schlüssel werden implizit in Zeichenketten umgewandelt. Zumindest bei Ganzzahlen ergibt das keinen Sinn, da das direkte hashen von Ganzzahlen wesentlich effizienter ist, als Zeichenketten neu zu erzeugen. Problematisch ist weiterhin, dass die eckigen Klammern bei der Umwandlung verloren gehen, die Schlüssel
[1, [2, 3]] und [1, 2, 3]
somit gleich sind.
Meines erachtens wäre folgende Syntax sinnvoller gewesen:
t = {"x": 1, "y": 2}; t = {x = 1, y = 2};
Hiermit lassen sich auch recht angenehm benannte Argumente (wie sie in Python und R zu finden sind) simulieren.
Möchte man in JavaScript ein wiederverwendbares Modul schreiben, so sollten dessen Funktionen in einem eigenen Namensraum stehen. Betrachten wir folgende Funtkionen die zunächst flach im Hauptnamensraum stehen:
function f(x){return 2*x;} function g(x){return 3*x;}
Das Modul möchten wir nun als M
bezeichnen. Die
erste Idee ist, die Funktionen einfach als Slots eines Objektes
M
aufzufassen.
var M = { f: function(x){return 2*x;}, g: function(x){return 3*x;} };
Falls das Modul nun statische Variablen besitzt, kann auf diese über das this-Argument zugegriffen werden:
var M = { name: "M", f: function(){return this.name;} };
Hiermit ist es auch möglich, Funktionen aus dem Modul innerhalb des Moduls aufzurufen. Hier ein Beispiel mit Rekursion:
var M = { f: function(x){return x == 0? 1 : 2*this.g(x-1);}, g: function(x){return x == 0? 1 : 3*this.f(x-1);} }
Leider kommen wir nun in einen Konflikt, wenn wir eine Funktion aus dem Modul als Argument an eine andere Funktion übergeben wollen.
var M = { beta: 2, f: function(x){return this.beta*x;}, g: function(a){return a.map(this.f);} };
Dieses Beispiel wird nicht wie erwartet funktionieren.
Bei der Übergabe von f
an map
geht
nämlich das this-Argument von f
verloren. Somit
kann nicht mehr auf beta
zugegriffen werden.
Die Direkte Lösung dieses Problems ist die feste Bindung des
this-Argumentes an f
. Dies geschieht durch Hinzufügen
von:
M.f = M.f.bind(M);
Man könnte anstelle des this-Argumentes auch direkt auf die Modulvariable zugreifen:
var M = { beta: 2, f: function(x){return M.beta*x;}, g: function(a){return a.map(M.f);} };
Hier muss aber beachtet werden, dass der Inhalt der Variable
M
nicht mehr verändert werden darf. Folgendes
ist also nicht möglich:
var M2 = M; M = undefined;
Die Lösung besteht darin, eine lokale Variable für das Modul einzuführen.
var M = (function(){ var M = { beta: 2, f: function(x){return M.beta*x;}, g: function(a){return a.map(M.f);} }; return M; })();
Man hätte auch alle Funktionen und Variablen direkt als lokale Variablen schreiben können.
var M = (function(){ var beta = 2; var f = function(x){return beta*x;}; var g = function(a){return a.map(f);}; var Interface = {g: g}; return Interface; })();
Hier ist alles privat, was sich nicht im Interface befindet.
Leider ist jetzt eine nachträgliche Manipulation des Moduls nicht mehr
möglich, z. B. wenn man das Modul mit M.beta=4
konfigurieren möchte. Es ist aber möglich, konfigurierbare Eigenschaften
des Moduls explizit zum Interface mit aufzunehmen.
var M = (function(){ var config = {beta: 2}; var f = function(x){return config.beta*x;}; var g = function(a){return a.map(f);}; var Interface = {g: g, config: config}; return Interface; })(); M.config.beta = 4;
Bisher haben wir nicht beachtet, dass es auch mehrere Instanzen eines Moduls geben könnte. Z. B. würde man von einem Modul gerne eine shallow copy machen und diese Kopie modifizieren. Alternativ kann das Prototypensystem hierfür verwendet werden.
Möchte man eine nachträgliche Manipulation verbieten, so kann das Modul als Factory formuliert werden. Eine solche nimmt eine Konfiguration und erzeugt aus dieser ein konfiguriertes Modul:
var M = (function(config){ var beta = config.beta; var f = function(x){return beta*x;}; var g = function(a){return a.map(f);}; var Interface = {g: g}; return Interface; }); var M1 = M({beta: 4});
An dieser Stelle soll noch bemerkt werden, dass lokale Variablen aus dem äußeren Kontext auch in Funktionen stehen können, bevor sie definiert wurden. Da Funktionen auch in lokalen Variablen gespeichert werden, erlaubt dies Rekursion ohne Formulierung von Prototypendeklarationen wie sie in Pascal und C erforderlich sind. Die Variablen werden nämlich erste beim Verlassen der äußeren Funktion bei der Closure-Bindung durch Kopien überschrieben, vorher waren es Referenzvariablen auf die tatsächlichen Variablen. In der Programmiersprache Lua ist diese Problematik unter dem Stichwort »upvalues« bekannt.
Der Methodenaufruf a.map(f)
kann eine Funktion
f
mit mehr als einem Argument annehmen. Zusätzlich
lässt sich hierdurch der Index des Array-Elements mit übergeben.
Wer auch immer das so entworfen hat, hat sich zu diesem Zeitpunkt
nicht die Gedanken über die Konsequenzen gemacht, die daraus
resultieren. Eine Funktion mit einem optionalen Argument bekommt
dann nämlich den Index anstelle von undefined
, was niemals
beabsichtigt war.
Eine klare Lösung geben strenge funktionale Programmiersprachen
vor. Dort gibt es eine Funktion enumerate
, welche
a[i]
auf [i, a[i]]
abbildet. Wenn das
nicht effizient genug ist, kann man immer noch eine for-Schleife
benutzen bzw. eine Funktion map_with_index
schreiben.