Mittwoch, 20. Juni 2007

JavaScript: Vorsicht mit Methodenreferenzen

Es ist ein empfohlenes Vorgehen, Funktionen in JavaScript nicht über ihren globalen Namen aufzurufen, sondern eine lokale Funktionsreferenz im aktuellen Kontext zu erzeugen. Das gilt vor allem für mehrfache Aufrufe. Hier ein unschönes Beispiel:
function buchen(iString) {
sendRecord( 1 );
sendRecord( 17 );
sendRecord( 21 );
...
}

Hier wird für jeden Funktionsaufruf ein globaler Lookup für das Symbol sendRecord durchgeführt. Es wird also in den lokalen Variablen, danach im "Scope" der Funktion buchen(), danach im übergeordneten "Scope" geschaut, bis das Symbol schliesslich im globalen Scope gefunden wird. Das ist zwar schnell, kann aber bei sehr vielen Funktionsaufrufen Kosten verursachen - vor allem wenn die Funktion im Browser läuft und die Suche sich daher auch auf das window-Objekt erstrecken muss.

Besser ist es hier, mit einer lokalen Funktionsreferenz zu arbeiten, wie in folgendem Beispiel:
function buchen(iString) {
var lSendRecord = sendRecord;
lSendRecord( 1 );
lSendRecord( 17 );
lSendRecord( 21 );
...
}

Nun ist lSendRecord eine Referenz auf die Funktion sendRecord, lebt aber als lokale Variable im "Stack". Das ist der Ort, wo der Lookup als erstes ausgeführt wird. Der globalen Lookup muss hier nur einmal durchgeführt werden, nämlich bei Evaluierung des Wertes von lSendRecord. Die aktuellen Aufrufe der Funktion laufen dann alle über das lokale Symbol. Das macht die JavaScript-Ausführung effizienter.

Vorsicht ist jedoch geboten, wenn man Methoden referenziert. Stellen wir uns der Einfachheit folgendes Singleton-Objekt s vor:
s = {
data:[1,2,3],
getData:function(iIndex) {
return this.data[iIndex];
}
};

Wenn wir nun wie oben einen lokale Methodenreferenz für die Funktion getData erzeugen wollen, bekommen wir ein Problem:
function buchen() {
var lGetData = s.getData;
var lValue = lGetData(1);
...
}

Dieser Code wird nicht wie gewünscht funktionieren. Grund ist die Verwendung des Schlüsselworts this im Rumpf der Methode getData(). Die Methodenreferenz ist lediglich ein Zeiger auf die Implementierung der Methode getData(). Das heisst, der Aufruf lGetData(1) führt den Code einfach genau so aus, wie er in dieser Implementierung angegeben ist. Das Schlüsselwort this wird dabei nicht auf das Objekt umgesetzt. Die Methodenreferenz ist ja eigentlich bloss eine Funktionsreferenz. Sie merkt sich nicht den Context, in dem sie aufgerufen wird, weiss also auch nicht, dass sie Instanzmethode des Singleton-Objekts s ist. Also bekommen wir ein Problem: this.data kann nicht korrekt aufgelöst werden, wenn this nicht auf s zeigt. Die Funktion bricht mit einem JavaScript-Fehler ab. Es ist einer dieser leidigen Fehler, von denen man nicht weiss, was eigentlich los ist (welches Objekt, welche Methode?)

Das Objekt unterstützt diese Eigenschaft oder Methode nicht.

Wie kann man obiges Beispiel also korrigieren? Indem man eine Objektreferenz statt einer Methodenreferenz verwendet.
function buchen() {
var lS = s;
var lValue = lS.getData(1);
...
}

Das ergibt zwar einen zweistufigen Lookup, aber beide sind schnell: der erste ist die Auflösung der lokalen Variablen lS, der zweite sucht die Komponente getData im Singleton s. Da s technisch nichts anderes als ein Hash ist, ist auch dieser zweite Lookup sehr schnell. Und, das wichtigste: Die Methode hat nun überhaupt erst die Chance, korrekt ausgeführt zu werden!

Noch eine Anmerkung zm Thema Indirektion. Noch besser wäre es, sich die Singleton-Instanz über eine get-Methode zu beschaffen, zum Beispiel so:
function buchen() {
var lS = getS();
var lValue = lS.getData(1);
...
}

Warum ist das besser? Weil man Implementierungen austauschen kann. Man kann "mocken" und "stubben", wenn man Tests für die Methode schreibt: Wenn man im Testkontext die getS()-Funktion so überschreibt, dass sie statt s eine Testinstanz derselben Signatur (also mit denselben Komponenten wie s) zurückgibt, kann man den aufrufenden Code isoliert testen (indem die Testimplementierung von s das zurückgibt, was man eben so von s erwartet).

Keine Kommentare :