Den Scope betrügen – wenn der Berg mal zum Propheten kommt

Javascript und der Scope (dt. “Sichtbarkeitsbereich“) einer Variablen ist so eine Sache.  Durch eine klitzekleine Fehlkonzeption – wenn man es denn so nennen kann – in ECMAScript (die Sache mit dem Schlüsselwort “this” und dessen änderbarer Referenz in “self”) fällt es hin und wieder schwer zu erkennen, in welchem Scope man sich gerade “herumtreibt”, also in welchem Kontext bspw. eine Funktion oder Instanzmethode initialisiert oder aufgerufen wird. Wobei eine Methode, die in Kontext a deklariert wird, noch lange nicht in Kontext a ausgeführt werden muss.

Natürlich erschwert die exzessive Verwendung von Closures die Nachverfolgung von Lebenszyklen sowie eventuelles Debugging noch zusätzlich. Dabei nicht berücksichtigt, dass solche Konstruktionen auch gerne mal den garbage collector älterer bzw. fehlerhafter Browser vollmüllen und so den Benutzer schnell mit Browserabstürzen (the good!) nerven oder (the bad!) ihn durch hundertprozentige Speicherauslastung zu einem Neustart zwingen.

Wann treten diese Probleme denn eigentlich auf? In der Regel immer dann, wenn man ereignisgesteuert programmiert, wozu agile Sprachen wie Javascript geradezu einladen. Denn wer sich einmal mit einer klassischen Beobachter-Implementierung bspw. in Java herumgeschlagen hat, der lernt sein kleines Javascript schnell zu schätzen – doch der Vergleich hinkt natürlich.

Ein kleines Beispiel aus der Dojo-Welt:

dojo.query('a').connect('onclick', console, 'log')

Sieht ziemlich einfach aus, birgt aber trotz der Prägnanz eine Menge Alternativen und damit Stolpersteine. Kurz erklärt: dojo.query() zieht via CSS3-Selektor alle Links und liefert sie als Instanz von dojo.NodeList. Auf dieser Rückgabe wird die methode connect() aufgerufen, die einen Event-Listener an jedes Element der NodeList bindet. NodeList.connect() nimmt wahlweise 2 oder 3 Argumente: In jedem Falle den Event-Namen vom Typ String (“onclick”), als zweites Argument eine Objekt- oder eine Funktionsreferenz und als drittes, optionales Argument einen Methodennamen vom Typ String (nur sinnvoll, wenn Argument No. 2 eine Objektreferenz ist) oder wiederum eine Funktionsreferenz (dann bestimmt Argument No. 2 die Objektreferenz, in dessen Kontext Argument 3 aufgerufen wird.).  Werden nur zwei Argumente übergeben, wird der Callback standardmäßig im Kontext des jeweiligen Elements der NodeList ausgeführt. Alles klar?

Damit zurück zum obigen Beispiel:

dojo.query('a').connect('onclick', console, 'log')

bindet natürlich an alle Links (also <a>-Tags) einen onclick-Eventhandler, der auf dem (globalen) Objekt console die Methode log() aufruft. Übrigens: Implizit erhält log() beim Aufruf ein Event-Objekt als einziges Argument.

Will man nun innerhalb seiner selbstdefinierten Javascript-Klassen Objektmethoden innerhalb eines Event-Listeners aufrufen, ginge dies mithilfe dieses Schweizer Ereignistaschenmessers also ganz einfach:

dojo.declare('my.MyClass', null, {
constructor: function()
{
// Übergebe this als Kontext
dojo.query('a').connect('onclick', this, function(ev)
{
// this bezieht sich auf die Instanz von my.MyClass
this.doOnClick(ev);
});
}
}

oder auch kurz:

dojo.declare('my.MyClass', null, {
constructor: function()
{
// Übergebe this als Kontext
dojo.query('a').connect('onclick', this, 'doOnClick';
}
}

Und so weiter. Was aber, wenn einem diese Möglichkeiten der flexiblen Bindung von Funktionsreferenzen an beliebige Objektreferenzen durch die Programm-API verwehrt werden?

So klassischweise beim XhrRequest, zum Beispiel hier:

new XhrPost({
// options
// ....
// onLoad callback
load: function()
{
// Wo zum Teufel ist mein Kontext?
this.doSomethingCustomOnLoad(arguments); // Fehler!
}
})

Es gibt bei der Erzeugung des Request-Objekts keine Möglichkeit, die Referenz auf den aufrufenden Kontext an den Callback durchzureichen. Oder? Nun, in der Regel behilft man sich, in dem man sowas wie

// ....
doSomethingCustomOnLoad : function()
{
// ...
},
doXhrRequest: function()
{
// Referenz auf das this-Objekt speichern
var self = this;
// XhrRequest instanziieren
new XhrPost({
// ....
// onLoad callback
load: function()
{
// Meinen Kontext habe ich in self gespeichert
self.doSomethingCustomOnLoad(arguments);
}
});
});
}

herunterrotzt.

Das  ganze ist nicht nur unelegant, sondern birgt auch die Gefahr, dass in self nicht zuverlässigerweise das gleiche Referenzziel steht, das weiter oben gebunden wurde. So zum Beispiel immer dann, wenn man die ganze Konstruktion in einer Schleife mehrmals abarbeiten würde. Dann muss noch eine Closure in jedem Schleifendurchlauf zwischengeschaltet werden, die den Scope eindeutig definiert. Sonst würde das self-Object mit jedem Schleifendurchlauf mit dem jeweilig nächsten Referenzwert überschrieben werden.

Die meisten Javascript Frameworks bieten aber auch etwas versteckt die Möglichkeit, weitere Eigenschaften (custom properties) durchzuschleifen und somit einen eleganteren Weg zu gehen. So auch dojo in der dojo.XhrPost-Klasse:

 // ....
doSomethingCustomOnLoad : function()
{
// ...
},
doXhrRequest: function()
{
// XhrRequest instanziieren
new XhrPost({
// ....
// Ich gebe dem Constructor den gewünschten Kontext
// als custom property mit.
thisObject: this,
// onLoad callback
load: function()
{
// Meinen Kontext finde ich nun im zweiten Callback-Argument. Nutze &lt;a href="http://www.webreference.com/js/column26/apply.html"&gt;apply()&lt;/a&gt;, um
// den Callback im Kontext des thisObjects auszuführen und die Callback-Argumente
// an doSomethingCustumOnLoad() durchzureichen.
var thisObject = arguments[1].args.thisObject;
thisObject.doSomethingCustomOnLoad.apply(thisObject, arguments);
}
});
});
}

Damit bin ich in der Lage, alle Eigenschaften meiner Instanz aus dem Callback heraus via this.doIrgendwas(); aufrufen. Dazu noch habe ich den Scope meiner Callback-Funktionsreferenz wohl definiert, womit ausgeschlossen ist, dass mir die Referenz durch einen falsch gesetzten Zeiger das Script verhaut.  Und ich bin nicht länger gezwungen, alle Eigenschaften der eigenen Klasse X, auf die ich im Kontext des XhrPost-Callbacks zugreifen möchte, in einem dritten, gemeinsamen und möglicherweise global sichtbaren Kontext zugänglich zu machen. Ein weiterer Vorteil ist zuletzt, dass ich nicht in Gefahr laufe, den Garbage Collector zu behindern, indem ich überall irgendwelche Referenzen namens “self”, “daswarichmal”, “dasbinichnoch” herum fliegen lasse. Ich spare also Aufräumarbeiten.

Das ist nur einer von vielen denkbaren Anwendungsfällen, in denen die nativen Methoden apply() bzw. call() (der einzige Unterschied zwischen den beiden besteht übrigens darin, dass apply() ein argument[]-array und call() eine Liste von Funktionsargumenten schluckt, ansonsten machen die so ziemlich dassselbe) einem viel Arbeit abnehmen. Noch dazu finde ich persönlich, dass sie jede Konstruktion viel transparenter machen, wenn man sich ganz einfach klar macht, dass man keine Referenzen mehr in einen bestimmten Scope schiebt, sondern den Scope einfach zu den Referenzen.

4 Replies to “Den Scope betrügen – wenn der Berg mal zum Propheten kommt”

  1. Cooler Artikel! Ich frag mich nur wieso eine lokale Variable innerhalb einer Methode, die auf this referenziert Probleme mitm Garbage collector macht? Wobei wenn das nur älteren bzw. fehlerhaften Browsern passiert ist das ja vernachlässigbar ^^

    (love that link ;))

  2. Netter Artikel, nur ein Hinweis: dojo bietet auch die Funktion dojo.hitch, mit der sich der Kontext des callbacks festsetzen lässt. Beispiel:


    load: dojo.hitch(this, function()
    {
    // Meinen Kontext habe ich einfach behalten
    this.doSomethingCustomOnLoad(arguments);
    }),

  3. Oha, vielen Dank! Sowas habe ich schon gesucht. – Da ist mir tatsächlich eher mein schlechtes Englisch im Weg, ich musste mir “Hitch” gerade tatsächlich mal ergooglen – passt aber zusammen, wenn man den Film kennt :D

Leave a Reply

Your email address will not be published. Required fields are marked *