Tim Tepaße: Namensauflösung in benannten Funktionen

Beitrag lesen

Hallo,

Aber, da es nur FF macht, vermute ich hier einen Bug. FF scheint in dem Fall, die Suche nach dem Kontextobjekt anders anzugehen
Normalerweise: window -> Object
hier: Object -> window

Ich guck bei so etwas immer gerne in der ECMAScript-Spezifikation nach, so schwierig und tröge zu lesen sie auch ist. ;-) Aber nur so kriegt man raus, wie es sein soll und wer davon abweicht und ob das wirklich zu verurteilen ist.

Erstmal, um klar zu werden, was da überhaupt passiert, das Ganze ist ein sogenannter Zuweisungsausdruck. Links vom Gleicheheitszeichen steht ein Ausdruck, rechts vom Gleichzeitszeichen steht ein Ausdruck, eine function expression, ein Funktionsausdruck, keine „benannte Methode“:

this.foo = function foo ( ) { bar(); };
             [----Funktionsausdruck----]

In der ECMAScript-Spezifikation ist die Syntax für Funktionsausdrücke so definiert:

FunctionExpression :
    "function"  'Identifier'?  "("  FormalParameterList?  ")"  "{"  FunctionBody  "}"

Ich habe hier die sogenannten Terminalsymbole, d.h. „richtige“ ECMAScript-Syntax, in doppelte Anführungszollzeichen gesetzt, selbst zu vergebende Namen in einfache Anführungsapostrophen gesetzt, Bestandteile, deren Syntax woanders definiert sind so gelassen, optionale Elemente mit einem folgendem Fragezeichen ausgestattet und Leerraum Leerraum gelassen. Die ECMAScript-Spezifikation hat mehr typographische Elemente zu Verfügung und macht das durchaus hübscher. Man kann es auch so lesen: „Ein Funktionsausdruck wird definiert durch die Zeichenfolge ‚function‘, einem optionalen, selbst zu vergebenden Identifizierer, einer öffnenden Klammer, einer Parameterliste, deren Syntax woanders definiert ist, einer schließenden Klammer, einer öffnenden geschweiften Klammer, dem die Funktion ausmachenden Code und einer schließenden geschweiften Klammer.“ Ich ziehe dann doch speziellere kleine Minisprachen für so etwas vor.

Wird nun so ein Funktionsausdruck ausgewertet, geschieht das je nach Vorhandensein des zusätzlichen Identifizierers unterschiedlich. Zuerst den Normalfall, ohne Identifizierer:

The production FunctionExpression : function ( FormalParameterList? ){ FunctionBody }
  is evaluated as follows:

1. Create a new Function object as specified in 13.2 with parameters specified by
  FormalParameterList? and body specified by FunctionBody. Pass in the scope chain of
  the running execution context as the Scope.
  2. Return Result(1).

Übersetzt:

„Erstelle ein neues Funktionsobjekt mit den optionalen Parametern, dem entsprechenden Code und nutze zur Namensauflösung innerhalb des Funktionsobjektes die um Zeitpunkt der Funktionsdefinition gültige Liste der Objekte, in denen nach Namen gesucht werden. Gib dieses Funktionsobjekt zurück.“

Ist jedoch ein Identifizierer für die Funktion vorhanden, gilt eine andere Definition zu Auswertung:

The production FunctionExpression : function Identifier ( FormalParameterListopt ) { FunctionBody }
  is evaluated as follows:

1. Create a new object as if by the expression new Object().
  2. Add Result(1) to the front of the scope chain.
  3. Create a new Function object as specified in 13.2 with parameters specified by
  FormalParameterListopt and body specified by FunctionBody. Pass in the scope chain of
  the running execution context as the Scope.
  4. Create a property in the object Result(1). The property's name is Identifier, value
  is Result(3), and attributes are { DontDelete, ReadOnly }.
  5. Remove Result(1) from the front of the scope chain.
  6. Return Result(3).

Übersetzt:

1. Erstelle ein neues, simples Objekt.
  2. Setze dieses Objekt an die Spitze der Liste der Objekte, in denen nach Namen
  gesucht werden soll.
  3. Erstelle ein neues Funktionsobjekt .. yadayadayada.
  4. In diesem geheimnisvollen Objekt erstelle eine einzige nicht lösch- und nicht
  überschreibbare Eigenschaft mit dem Namen gleich dem Identifizierer, die auf das
  Funktionsobjekt zeigt.
  5. Mach die Namensauflösungsliste wieder auf normal; wir wollen ja niemanden stören
  6. Gib das Funktionsobjekt zurück.

In solchen Funktionsobjekten geschieht die Namensauflösung also anders. In der Liste von Objekten in den nach Namen gesucht werden soll, findet sich immer an erster Stelle ein Objekt, das den Namen der Funktion beinhaltet und damit auf die Funktion selber zeigt. Oder noch simpler: In benannten Funktionen zeigt der Funktionsname immer auf die Funktion selber – und man kann das nicht ändern.

Warum man das macht? Um rekursiv programmieren zu können, braucht man immer eine Referenz auf die Funktion selber; in ECMAScript hat man sich vermutlich eben wegen der ziemlich starken Dynamik der Namensauflösung für diese Lösung entschieden. Eine andere denkbare Lösung wäre z.B. ein spezielles Schlüsselwort wie „func“ gewesen, das immer auf die Funktion selber zeigt, analog zu „this“, dass immer auf das Objekt zeigt, in dessen Kontext die Funktion ausgeführt wird. Aber ist halt nicht. Und eigentlich ist es doch auch eine ganz vernünftige Lösung.

Was passiert nun beim Methodentester?

~~~javascript function ClassA ( ) {
      this.methodX = function methodX () {
          foo();
      };
  }

  
Das generierte Funktionsobjekt methodX hat nun dank der obigen Definition eine eigene scope chain. Diese Liste von Objekten, in denen nach Namen gesucht wird, sieht dann vereinfacht so aus:  
  
  `[Obj, <scope von ClassA> window]`{:.language-javascript}  
  
„Obj“ ist das neue anonyme Objekt; in JSON-ähnlicher Notation sieht es so aus: { "methodX" : <funktionsObjekt> }. Wenn nun ein Name in der Funktion gesucht wird, wird in der Liste von Objekten nach diesem Namen erst in Obj gesucht, dann im Geltungsbereich von ClassA, als letztes in window. Es wird aber ganz klassisch nach Namen in Objekten gesucht. Jedes, aber auch wirklich jedes Objekt ist prototypisch mit der Eigenschaft „foo“ erweitert worden, d.h. wenn man in jedem Objekt nach „foo“ sucht, findet man auch „foo“. Und das gilt auch für unser anonymes Objekt obj; es wird innerhalb methodX immer object.prototype.foo gefunden.  
  
  ~~~javascript
function ClassA ( ) {  
      this.methodY = function () {  
          foo();  
      };  
  }

Hier dagegen sieht die scope chain so aus: [<scope von ClassA], window]. Im Geltungsbereich von ClassA findet sich kein „foo“, also wird in window gesucht. Und im window-Objekt wird window.foo vor window.prototype.foo gefunden, weil so nun mal die Findung von Eigenschaften in prototypisch erweiterten Objekten funktioniert, erst wird im Objekt gesucht und dann im Prototyp, dann in dessen Prototyp, etc. Wie so oft schießt man sich in den eigenen Fuß, wenn man object.prototype verändert.

Warum kann ich mir nicht erklären, kein anderer Browser macht das so.

Firefox macht es mal wieder als einziger richtig. ;)

Tim