Probleme mit der Gültigkeit von Variablen in JavaScript

Bei meinem aktuellen Bastelprojekt habe ich mich in die Tiefen von AJAX gestürzt. Ich hab schon viel geflucht, aber das hier ist die Krönung.

Man stelle sich vor, man möchte an einer Reihe von gleichartigen Elementen Listener registrieren, sodass an jedem Element eine leicht veränderte Aktion ausgeführt wird. Das geht zum Beispiel mit prototype an sich ziemlich einfach. Es kommt unter Umständen folgener Code heraus (der hier natürlich auf das Essentielle vereinfacht ist):

for ( i=0; i<4; i++ ) {
  var j = 2*i;
  $('button_' + i).observe('click', function (event) { alert(j); });
}

Dieses unschuldige Stückchen Code hat aber nicht den gewünschten Effekt. Jeder Button meldet ’6′. Das ist verwirrend, denn jeder Button bekommt seine eigene Funktion, die von einem Wert abhängt, der ganz sicher auch nur für die Konstruktion dieser einen Funktion benutzt wird. Natürlich muss j nach Ende jeder Iteration weiterleben, damit die anonym definierte Funktion einen sinnvollen Kontext hat. Letztenendes muss es also vier Instanzen von j geben, die alle die Schleife überdauern.
Ich vermute, dass der Interpreter meint, zu optimieren, indem er j immer wieder überschreibt. In vielen Fällen mag das sinnvoll sein, wenn j nämlich nach Ende einer Iteration tatsächlich nicht mehr lebt. Hier sind aber wirklich vier Instanzen nötig.

Man kann das nun in den Griff bekommen, indem man zum Beispiel so arbeitet:

for ( i=0; i<4; i++ ) {
  var j = 2*i;
  $('button_' + i).observe('click', clicked(j));
}

function clicked(i) {
  return function(event) { alert(i); };
}

Ist das ein Bug von Javascript oder nur von Firefox (3.0.8) oder ist das ein Feature und ich bin zu dämlich?

Codebeispiele hier.

5 Comments.

  1. Hi!

    Wie Du ja schon festgestellt hast, ist die Frage hier, welche Variablen hier welchen Scope haben. Je nach Scope der Variable umschliessen dann die erstellten Closures die selbe oder unterschiedliche Variablen. Eine ähnliche, ganz lesenswerte, Diskussion habe ich vor kurzem auch erst hier gelesen:

    http://math.andrej.com/2009/04/09/pythons-lambda-is-broken/

    Kommentar 3 zu diesem Artikel geht auch auf JavaScript ein. Eine kurze Google-Suche zum Thema “javascript closure weirdness” gibt dann auch folgendes Ergebnis:

    http://weblogs.asp.net/bleroy/archive/2006/02/24/438990.aspx

    Um die Zusammenfassung dieses Artikels kurz zu zitieren: “variables in JavaScript are only local to functions, not to any other kind of {} block.” Zu gut Deutsch, wenn Du “var j” im Schleifenrumpf schreibst, bedeutet das eigentlich “var j” auf Funktionsebene. (Echt behämmert. Man sollte in der Tat annehmen, dass durch explizite Variablendeklarationen a la “var x” klargestellt werden kann, in welchem Scope eine Variable gültig ist. Historisch ist das wohl darauf zurückzuführen, dass JavaScript ursprünglich keine Closures hatte, da fallen solche Ungereimtheiten nicht so schnell auf…)

    Nebenbei erwähnt, Ruby ist in Closure-Hinsicht auch nicht ganz optimal gelöst:

    def f
    x = “Trullala”
    puts x
    puts [12345].map{ |x| x+1 }
    puts x
    end

    Beim zweiten “puts x” hat hier x den Wert 12345, weil das Funktionsargument x die selbe Variable ist wie das x, das lokal zu f ist.

    Man kann bei Closures viel falsch machen, es gibt ein paar Sprachen die sich mit einigen historischen Kompatibilitätsaltlasten rumplagen. Ganz hübsch finde ich die Lösung in Smalltalk: Das Parameter-Problem wie in Ruby gibt es dort nicht. Den Scope einer Variable kann man wie in JavaScript über den Ort der Deklaration steuern. Zwar unterstützen nicht alle Implementierungen lokale Variablen in anonymen Funktionen, aber zumindest passiert nichts unerwartetes. :-)

    Ist ein ziemlich interessantes Thema, finde ich. Wenn Du das spannend findest, überleg’ Dir auch mal, wie Non-Local-Returns implementiert sind, und was da die Gemeinsamkeiten mit Exception-Handling sind. Die Erkenntnis ging mir ziemlich ab, als ich mal drüber gestolpert bin. :-)

    Viele Grüße,
    Günther

  2. Wow, danke für deinen ausführlichen Kommentar!

    Blöcken keinen eigenen Gültigkeitsraum zu geben finde ich sehr, sehr seltsam. Gut, wenn man es weiß, kann man damit leben. Aber gerade wenn man, wie ich, von Java kommt und Trial-and-Error versucht, kann man schon an solchen Phänomenen verzweifeln, weil es auch schwer ist, sowas zu googlen.

    Das Rubybeispiel ist natürlich auch gruselig. Ein Closure verstehe ich schon als Funktion, da sollte es einen eigenen Scope geben!

    In meinem Beispiel liegt aber ein leicht anderer Fall vor. Denn selbst, wenn j seinen Wert ändert, würde ich erwarten, dass mein Funktionsobjekt mit den Werten zur Zeit seiner Erstellung bestückt bleibt. Denke ich da zu funktional? Javascript macht anscheinend eine Funktion dauerhaft abhängig von ihrem Deklarationskontext, der sich auch im Nachhinein noch ändern darf.

    Insgesamt wirkt das nicht gut durchdacht. Oder braucht man das an irgendeiner Stelle? In der Form ist das doch kaum beherrschbar. Ich bin nur aufgrund von Wissen aus Vorlesungen drauf gekommen, was hier los sein könnte; wer außer Studierten und Leuten, die wirklich tief in der Sprachmaterie stecken, durchblickt denn das und findet überhaupt raus, was er “falsch” macht?

    Was ist denn ein non-local-return? Gleich mal nachsehen…

    Edit: Nachgesehen. Wenn ich das richtig verstanden habe, ist der non-local-return intuitiv, wenn man sich vorstellt, dass der Code aus dem Closure in der umschließenden Funktion expandiert wird. Wenn man davon ausgeht, dass ein Closure eine Funktion ist, bekommt man natürlich Probleme. Insofern wäre da ein syntaktisches Mittel angebracht, um klar zu machen, welche Funktion man beenden möchte, oder man schafft non-local-return ab. Oder gibt es Szenarien, wo man es wirklich braucht? Schon PH predigte in SE1, dass man möglichst nur ein return pro Funktion/Prozedur/Methode benutzen sollte.
    Ähnliche “Probleme” hat man ja auch mit break in geschachtelten Schleifen. Wie schön wäre es manchmal, angeben zu können, bis zu welcher Tiefe man herausmöchte…

  3. Ja, Du denkst wahrscheinlich etwas zu funktional. ;-)

    In der funktionalen Programmierung gilt ja die “Single Assignment Rule”, also daß jede Variable nur an einen Wert gebunden wird, es gibt also keinen veränderlichen Zustand. So kann in der Implementierung einer rein funktionalen Sprache einfach der Wert einer lokalen Variable in der Closure “eingeschlossen” werden.

    Anders ist es in der imperativen Welt, wo es veränderliche Zustände gibt. Plötzlich wirft sich da halt die Frage auf: Was passiert, wenn man aus der anonymen Funktion heraus eine _Zuweisung_ an eine solche “in der Closure eingefangene” Variable macht? Betrachte mal folgendes Beispiel (ich rücke mal mit _ ein, damit man es besser lesen kann):

    def f(array)
    __zaehler = 1
    __array.each do |x|
    ____zaehler = zaehler + x
    __end
    __return zaehler
    end

    Wie Du ja weisst, ist do…end die literalschreibweise für eine anonyme Funktion. (Tatsächlich handelt es sich hier um einen Block (kann nur “den Stack runter” gereicht werden), nicht um eine Closure. Die Variablenbindung verhält sich aber meines Wissens gleich.)

    Bei Beispielen wie diesem macht die Veränderung der eingeschlossenen Variablen plötzlich viel mehr Sinn. Und solcher Code ist ja sicherlich auch keine Seltenheit. Tatsächlich werden anonyme Funktionen also wie Du schon sagtest, an ihren “Deklarationskontext” (also den Stack Frame a.k.a Activation Record) gebunden, nicht an die Werte, die dort zur Zeit der Erstellung des Funktionsobjektes gespeichert sind.

    Eine andere, noch coolere Implikation dieser Sache ist, dass man auch so was machen kann:

    def make_iterator
    __i = 0
    __lambda do
    ____i = i + 1
    ____i
    __end
    end

    it1 = make_iterator
    it2 = make_iterator
    puts “it1 ist ” + it1.call.to_s
    puts “it2 ist ” + it2.call.to_s
    puts “it2 ist ” + it2.call.to_s
    puts “it1 ist ” + it1.call.to_s

    Was meinst Du, was dabei rauskommt? :-)

    Und nein, das ist nicht abartig. ;-) Damit kann man sehr hübsche Sachen machen, z.B. ein Objektorientiertes Framework hochziehen. Hier ein Konstruktor für eine Ente:

    def make_ente(name, art)
    __lambda do |methodenname|
    ____if methodenname == :quake
    ______lambda do
    ________puts “Quak, quak, ich bin die #{art}-Ente #{name}!”
    ______end
    ____elsif methodenname == :umbenennen
    ______lambda do |neuername|
    ________name = neuername
    ______end
    ____end
    __end
    end

    Und so kann man das benutzen:

    >> require ‘test’
    => true
    >> e = make_ente(‘Fridolin’, ‘Badewannen’)
    => #
    >> e.call(:quake).call
    Quak, quak, ich bin die Badewannen-Ente Fridolin!
    => nil
    >> e.call(:umbenennen).call(‘Ernst’)
    => “Ernst”
    >> e.call(:quake).call
    Quak, quak, ich bin die Badewannen-Ente Ernst!
    => nil
    >>

    Wenn man jetzt noch Lisp-Makros in den Topf wirft, ist das auch syntaktisch in Ordnung. :-)

    Hach, ich liebe kindische Beispiele! :-)

    Gruß,
    Günther

  4. Was du in der Methode f machst, ist natürlich erwünscht. Variablen sollten in Unterblöcken sichtbar sein.
    In deinem Beispiel oben sollte aber das x im Closure das außenliegende verschatten, finde ich. Schließlich ist ‘|x|’ ein klares Statement: ‘Ich möchte einen Parameter x haben!’, und als solcher sollte es auch vom umschließenden Block unabhängig sein.

    Deine weiteren Beispiele sind natürlich hübsch, aber warum sollte man Objekte durch Funktionen simulieren in einer Sprache, die Objekte hat? Wenn das die Anwendung ist, verzichte ich gerne zugunsten größerer Klarheit.

    Anbei bemerkt sehe ich noch nicht, warum deine Beispiele das Verhalten von Javascript im oben beschriebenen Fall nach sich ziehen.

    Ich habe immer noch das Gefühl, etwas Wesentliches zu übersehen.

  5. Ich stimme Dir zu, dass in dem “12345″-Beispiel ganz oben das x-Argument das x im umgebenden Scope einfach nur überschatten sollte, anstatt es zu überschreiben. Das ist einfach ein Manko an Ruby. Das weiss Matz sicher auch, aber im Nachhinein kann man solche Sachen nur schwer geradeziehen ohne massenweise Ruby-Programme kaputt zu machen. ;-)

    Das Javascript-Verhalten ist auch seltsam, und ich nehme an, dass die Designer sich ebenso über die Probleme bewußt sind, sie aber nicht geradeziehen können. ;-) Ergo, if I’m ever going to design a programming language… usw. Man muss sich über die Scoping-Probleme möglichst früh bewußt werden, um es nicht zu vermurksen. ;-)

    Natürlich würde man nicht unbedingt ein Objektsystem hochziehen, wenn die Sprache bereits ein anderes bereitstellt. Das ist ja nur in Ruby, weil ich weiss, dass wir beide die Syntax kennen. Wenn Du aber für eine Sprache ein Objektsystem hochziehen willst, dann geht das mit Lambdas ziemlich einfach. Tatsächlich wird es auch in einigen Sprachen so gemacht, und diese Objektsysteme integrieren sich dann auch deutlich besser in die Sprache als dieses kleine mit der Ente es tut. ;-)

    Das JavaScript-Scoping ist IMHO einfach ein bisschen b0rken. Würde mich natürlich trotzdem interessieren, wenn Dir noch Erleuchtungen zu dem Thema kommen. :)

    Gruß,
    Günther