molily.de

Die Grundlagen von JavaScript

Deutsche Übersetzung des Artikels JavaScript. The core. von Dmitry A. Soshnikov

Dieser Artikel ist eine Übersicht und Zusammenfassung dessen, was wir in der Artikelserie ECMA-262-3 in detail gelernt haben. Jeder Abschnitt enthält Verweise auf die jeweiligen Kapitel der ECMAScript-3-Reihe, die Sie bei Interesse lesen können, um ein tieferes Verständnis und weitere Erklärungen zu bekommen.

Die Zielgruppe dieses Artikels sind erfahrene Programmierer und Experten.

Wir beginnen mit der Betrachtung des Konzepts des Objekts, welches die Grundlage von ECMAScript bildet.

Das Objekt

ECMAScript ist eine stark abstrakte, objektorientierte Sprache, die mit Objekten arbeitet. Es gibt zwar auch Primitives (einfache Werte), aber diese werden bei Bedarf in Objekte umgewandelt.

Ein Objekt ist eine Sammlung von Eigenschaften und hat ein einziges Prototyp-Objekt. Der Prototyp kann ebenso ein Objekt sein oder der Wert null.

Fangen wir mit einem einfachen Schaubild eines Beispiel-Objekts an, mit dem wir in den folgenden Erklärungen arbeiten werden. Der Prototyp eines Objekts wird über die interne Eigenschaft [[Prototype]] referenziert. Trotzdem werden wir in den Diagrammen die Schreibweise __interne-Eigenschaft__ verwenden. Im Falle des Prototype-Objekts ist das __proto__. Das ist eine nicht standardisierte Eigenschaft, die allerdings in manchen ECMAScript-Interpretern wie SpiderMonkey (damit u.a. in Firefox) tatsächlich existiert.

Betrachten wir folgendes Codebeispiel:

var foo = {
  x: 10,
  y: 20
};

Wir haben hier zwei explizite, eigene Eigenschaften und eine implizite Eigenschaft __proto__. Diese ist der Verweis (die Referenz) auf den Prototyp von foo:

Abbildung 1. Ein einfaches Objekt mit einem Prototypen.

Wofür werden diese Prototypen gebraucht? Um diese Frage zu beantworten, beschäftigen wir uns mit dem Konzept der Prototyp-Kette.

Die Prototyp-Kette

Prototyp-Objekte sind gewöhnliche Objekte und können wiederum selbst eigene Prototypen besitzen. Wenn ein Prototyp einen Verweis auf einen weiteren Prototyp besitzt, der nicht null ist, spricht man von einer Prototyp-Kette.

Eine Prototyp-Kette (prototype chain) ist eine endliche Kette von Objekten, die verwendet wird, um Vererbung und gemeinsame Eigenschaften umzusetzen.

Stellen wir uns zwei Objekte vor, die sich nur in einem kleinen Teil unterscheiden und sich in den restlichen Teilen gleichen. In einem gut entworfenen System wollen wir diese gleiche Funktionalität selbstverständlich wiederverwenden, ohne den Code für jedes Objekt zu wiederholen. In klassenbasierten Systemen wird die Programmiertechnik der Code-Wiederverwendung klassenbasierte Vererbung genannt: Man bringt die gemeinsame Funktionalität in Klasse A unter und legt die Klassen B und C an, welche von A erben und zudem eigene kleine Änderungen enthalten.

ECMAScript kennt das Konzept der Klasse nicht. Dennoch unterscheidet sich die Art der Code-Wiederverwendung nicht groß, sie ist lediglich in mancher Hinsicht flexibler: Wiederverwendbarer Code wird über die Prototyp-Kette erreicht. Diese Art der Vererbung wird auf Delegation basierte Vererbung genannt (im Falle von ECMAScript genauer Prototyp-basierte Vererbung).

Wie im Beispiel mit den Klassen A, B und C, erzeugt man in ECMAScript Objekte: a, bund c. Objekt a speichert die Gemeinsamkeiten der Objekte b und c. Diese wiederum speichern lediglich ihre besonderen, zusätzlichen Eigenschaften oder Methoden, die sie voneinander unterscheiden.

// Das folgende Beispiel funktioniert in Firefox (SpiderMonkey),
// Chrome (V8) und Safari (JavaScriptCore/Squirrelfish)

var a = {
  x: 10,
  calculate: function (z) {
    return this.x + this.y + z
  }
};

var b = {
  y: 20,
  __proto__: a
};

var c = {
  y: 30,
  __proto__: a
};

// Rufe die vererbte Methode auf
b.calculate(30); // 60
c.calculate(40); // 80

Ziemlich einfach, oder? Das Beispiel zeigt, dass b und c Zugriff auf die Methode calculate haben, welche beim Objekt a definiert ist. Eben das macht die Prototyp-Kette möglich. Die Regel ist einfach: Wenn eine Eigenschaft oder Methode nicht bei einem Objekt selbst gefunden wird, d.h. wenn das Objekt keine eigene Eigenschaft dieses Namens besitzt, dann wird versucht, diese Eigenschaft/Methode in der Prototyp-Kette zu finden. Wenn die Eigenschaft auch nicht beim Prototyp gefunden wird, dann wird dessen Prototyp berücksichtigt und so weiter, bis die gesamte Prototyp-Kette durchlaufen wurde. (Übrigens passiert dasselbe bei klassenbasierter Vererbung beim Auflösen einer vererbten Methode: Dort wird die Klassen-Kette durchgegangen.) Beim Durchsuchen der Prototyp-Kette wird die erste Eigenschaft/Methode mit dem gesuchten Namen verwendet. Diese wird vererbte Eigenschaft genannt. Wenn eine Eigenschaft beim Nachschlagen in der Prototyp-Kette nicht gefunden wird, dann ergibt der Ausdruck den Wert undefined.

Beachten Sie, dass das Schlüsselwort this in einer vererbten Methode immer auf das Originalobjekt zeigt, nicht auf das (Prototyp-)Objekt, bei dem die Methode gefunden wurde. Das heißt, this.y im Beispiel verweist auf die Eigenschaft y von den Objekten b und c, nicht vom Objekt a. Hingegen wird bei this.x die Eigenschaft x vom Objekt a verwendet – hier kommt die Prototyp-Kette zum Einsatz. (Den this-Wert werden wir später noch genauer unter die Lupe nehmen.)

Wenn der Prototyp eines Objekts nicht ausdrücklich angegeben ist, dann wird der Standard-Wert für __proto__ verwendet – das ist das Objekt Object.prototype. Dieses hat ebenfalls eine Eigenschaft __proto__. Sie ist allerdings das letzte Glied in der Prototyp-Kette, denn sie hat den Wert null.

Das folgende Schaubild zeigt die Vererbungshierarchie unserer Objekte a, b und c:

Abbildung 2. Eine Prototyp-Kette.

Oftmals braucht man Objekte mit derselben oder einer ähnlichen Struktur (d.h. derselben Menge an Eigenschaften), aber unterschiedlichen Status-Variablen. In diesem Fall lässt sich eine Konstruktorfunktion verwenden, die Objekte anhand eines angegebenen Musters erzeugt.

Der Konstruktor

Neben dem Erzeugen von Objekten gemäß eines vordefinierten Musters hat eine Konstruktorfunktion noch einen weiteren Nutzen: Sie setzt automatisch das Prototyp-Objekt für die neu erzeugten Objekte. Das dazu verwendete Prototyp-Objekt kommt aus der Eigenschaft Konstruktorfunktion.prototype.

Das heißt, wir können das obige Beispiel mit den Objekten b und c mithilfe einer Konstruktorfunktion neu schreiben. Die Rolle des Objekts a (dem Prototyp) spielt nun Foo.prototype:

// Die Konstruktorfunktion
function Foo(y) {
  // Sie erzeugt Objekte mithilfe eines angegebenen Musters:
  // Die Objekte haben nach ihrer Erzeugung eine eigene Eigenschaft »y«
  this.y = y;
}

// Zudem ist in »Foo.prototype« ein Verweis auf den Prototyp aller
// neu erzeugten Objekte gespeichert. Wir können diese Eigenschaft nutzen,
// um gemeinsame, vererbte Eigenschaften oder Methoden zu definieren.
// Analog zum ersten Beispiel notieren wir:

// Vererbte Eigenschaft »x«
Foo.prototype.x = 10;

// Vererbte Methode »calculate«
Foo.prototype.calculate = function (z) {
  this.x + this.y + z;
};

// Nun erzeugen wir unsere Objekte »b« und »c«,
// Foo dient als Muster bzw. Vorlage:
var b = new Foo(20);
var c = new Foo(30);

// Rufe die vererbte Methode auf
b.calculate(30); // 60
c.calculate(40); // 80

// Überprüfen wir, ob die korrekten Eigenschaften referenziert werden:

console.log(

  b.__proto__ === Foo.prototype, // true
  c.__proto__ === Foo.prototype, // true

  // Zusätzlich zum Verweis auf den Prototyp wird die besondere Eigenschaft
  // »constructor« angelegt. Das ist ein Verweis auf die Konstruktorfunktion
  // selbst. Die Instanzen »b« und »c« können diese Eigenschaft per
  // Delegation erreichen und darüber auf ihren Konstruktor zugreifen.

  b.constructor === Foo, // true
  c.constructor === Foo, // true
  Foo.prototype.constructor === Foo // true

  b.calculate === b.__proto__.calculate, // true
  b.__proto__.calculate === Foo.prototype.calculate // true

);

Folgendes Modell veranschaulicht die Beziehungen zwischen den Objekten:

Abbildung 3. Ein Konstruktor und die Objektbeziehungen.

Diese Abbildung zeigt erneut, dass jedes Objekt einen Prototyp besitzt. Die Konstruktorfunktion Foo besitzt ebenfalls seinen eigenen __proto__-Verweis, welcher seinerseits über __proto__ auf den obersten Prototyp Objekt.prototype verweist. Wie gesagt ist Foo.prototype eine besondere Eigenschaft des Funktionsobjekts Foo, welche auf den Prototyp von b und c verweist.

Anmerkung der Übersetzung: Verwechseln Sie die Eigenschaften __proto__ und prototype nicht. __proto__ ist eine Eigenschaft eines jeden Objekts, die einen Verweis auf dessen Prototyp-Objekt enthält. Sie ist wie gesagt nicht standardisiert und daher nicht in allen ECMAScript-Interpretern verfügbar. Sie spiegelt die interne Eigenschaft [[Prototype]] wieder. prototype hingegen ist eine Eigenschaft von Funktionen (Funktionsobjekten). Sie enthält das Objekt, das als Prototyp für die Objekte dient, die beim Aufruf der Funktion mittels new erzeugt werden.

Rein formal gesehen, aus Sicht einer »Klassifizierung«, könnte die Kombination aus Konstruktorfunktion und Prototyp-Objekt auch »Klasse« genannt werden. Denn wir haben soeben ein neues, gesondertes Ding Foo erzeugt. Tatsächlich verwenden beispielsweise die dynamischen Klassen in der Programmiersprache Python exakt eine solche Auflösung der Eigenschaften/Methoden. So gesehen sind Klassen in Python lediglich »syntaktischer Zuckerguss« für eine auf Delegation basierende Vererbung, wie sie in ECMAScript Anwendung findet.

Eine komplette und detaillierte Erklärung dieses Themas findet sich in Kapitel 7 der ES3-Artikelserie. Dieses besteht aus zwei Teilen: Chapter 7.1. OOP. The general theory, der die verschiedenen OOP-Paradigmen und -Arten beschreibt und mit ECMAScript vergleicht, sowie Chapter 7.2. OOP. ECMAScript implementation, der sich ausschließlich mit OOP in ECMAScript beschäftigt.

Nachdem wir die Grundlagen von Objekten beleuchtet haben, schauen wir uns an, wie die Programmausführung durch einen ECMAScript-Interpreter geregelt ist. Diese ist durch den Stapel von Ausführungskontexten (execution context stack) bestimmt. Jedes Elements dieses Stapels kann ebenfalls als abstraktes Objekt dargestellt werden. Ja, an fast allen Stellen operiert ECMAScript mit dem Konzept des Objekts. ;)

Der Stapel von Ausführungskontexten

Es gibt drei Typen von ECMAScript-Code: globaler Code, Funktionscode und eval-Code. Sämtlicher Code wird in einem zugehörigen Ausführungskontext (execution context) ausgewertet. Während es nur einen globalen Kontext gibt, sind zahlreiche Funktions- und eval-Kontexte möglich. Bei jedem Funktionsaufruf wird in den Ausführungskontext der aufgerufenen Funktion gesprungen und der Funktionscode darin ausgeführt. Ebenso erzeugt jeder Aufruf der vordefinierten Methode eval einen eval-Ausführungskontext und führt den Code darin aus.

Dabei ist zu beachten, dass eine Funktion eine unendliche Menge an Kontexten erzeugen kann, denn bei jedem Funktionsaufruf – selbst dann, wenn die Funktion sich selbst rekursiv aufruft – wird ein neuer Kontext mit einem neuen Kontext-Zustand (context state) erzeugt:

function foo(bar) {}

// Rufe dieselbe Funktion mehrfach auf. Dabei werden drei Kontexte mit drei
// unterschiedlichen Kontext-Zuständen erzeugt, denn der Wert des Parameters
// »bar« ändert sich.

foo(10);
foo(20);
foo(30);

Ein Ausführungskontext kann andere Kontexte aktivieren, beispielsweise kann eine Funktion eine andere aufrufen, der globale Kontext kann wiederum den globalen Kontext aufrufen usw. Intern ist das als Stapelspeicher (Stack) umgesetzt, genannt Stapel von Ausführungskontexten.

Ein Kontext, der einen anderen Kontext aktiviert, wird Aufrufer (caller) genannt. Der aktivierte Kontext heißt Aufgerufener (callee). Ein aufgerufener Kontext kann gleichzeitig der Aufrufer weiterer Kontexte sein, beispielsweise wenn eine Funktion vom globalen Kontext aus aufgerufen wird und dann ihrerseits eine interne, verschachtelte Funktion aufruft.

Wenn ein Aufrufer an einen Aufgerufenen übergibt, dann wird die Ausführung des Aufrufers angehalten und der Kontrollfluss dem Aufgerufenen übergeben. Der Aufgerufene wird auf den Stapel gelegt und wird zum aktiven, gegenwärtig laufenden Ausführungskontext. Nachdem sich der aufgerufene Kontext beendet, gibt er die Kontrolle an seinen Aufrufer zurück und die Ausführung des Aufrufers wird fortgesetzt bis zu dessen Ende, und so weiter. Ein aufgerufener Kontext kann einfach zurückspringen (return) oder sich mit einem Ausnahmefehler (exception) beenden. Ein Ausnahmefehler, der nicht behandelt wird, kann einen oder mehrere Kontexte beenden, welche dann vom Stapel entfernt werden.

Das bedeutet, die Laufzeit eines ECMAScript-Programms kann als Stapel von Ausführungskontexten beschrieben werden, an deren Spitze der gerade aktive Kontext liegt:

Abbildung 4. Der Stapel von Ausführungskontexten.
(EC = execution context = Ausführungskontext, ECN = der n-te Ausführungskontext)

Wenn ein Programm startet, wird in den globalen Ausführungskontext gesprungen, das ist der erste und unterste auf dem Stapel. Der globale Code sorgt für Initialisierung und erzeugt die notwendigen Objekte und Funktionen. Bei der Abarbeitung des globalen Kontexts kann dessen Code einige bereits erzeugte Funktionen aufrufen. Daraufhin wird in deren Ausführungskontext gesprungen und dem Stapel werden neue Kontexte hinzugefügt. Nachdem die Initialisierung fertig ist, pausiert der Interpreter und wartet auf Ereignisse (z.B. Mausklicks), welche weitere Funktionen aufrufen und neue Ausführungskontexte erzeugen.

Im nächsten Schaubild kommen zwei Kontexte vor, ein Funktionskontext namens »EC1« und der globale Kontext, abgekürzt mit »Globaler EC«. Das Bild zeigt die Änderung des Stapels beim Aktivieren des Funktionskontexts sowie bei dessen Verlassen:

Abbildung 5. Der Ausführungskontext ändert sich.
(EC = execution context = Ausführungskontext)

Auf diese Weise regelt ein ECMAScript-Interpreter die Ausführung von Code.

Mehr Informationen zu Ausführungskontexten in ECMAScript finden Sie im passenden Kapitel Chapter 1. Execution context.

Wie wir bereits gesehen haben, kann jeder Ausführungskontext als ein Objekt dargestellt werden. Betrachten wir nun dessen Struktur und die Zustände (Eigenschaften), die zur Ausführung des Codes notwendig sind.

Der Ausführungskontext

Einen Ausführungskontext können wir uns abstrakt als gewöhnliches Objekt vorstellen. Jeder Ausführungskontext hat eine bestimmte Anzahl an Eigenschaften (Zustände des Kontexts), die notwendig sind, um den Fortschritt der Codeausführung nachzuhalten. Die folgende Abbildung zeigt die Struktur eines Kontexts:

Abbildung 6. Die Struktur eines Ausführungskontexts.

Neben diesen drei notwendigen Eigenschaften – dem Variablenobjekt, dem this-Wert sowie der Scope-Chain –, kann ein Ausführungskontext je nach ECMAScript-Interpreter beliebige weitere Zustände besitzen.

Schauen wir uns diese drei wichtigen Eigenschaften im Detail an.

Das Variablenobjekt

Ein Variablenobjekt ist der Gültigkeitsbereich (Scope) von Daten, die dem Ausführungskontext zugehörig sind. Dies ist ein spezielles Objekt, das die Variablen und Funktionsdeklarationen speichert, die innerhalb dieses Kontexts definiert werden.

Übrigens werden Funktionsausdrücke (function expressions) im Gegensatz zu Funktionsdeklarationen (function declarations) nicht im Variablenobjekt gespeichert.

Das Variablenobjekt ist ein abstraktes Konzept: Je nach Kontext-Typ haben wir es mit verschiedenen einzelnen Objekten zu tun. Im globalen Kontext beispielsweise ist das Variablenobjekt das globale Objekt selbst. Daher ist es möglich, globale Variablen als Eigenschaften des globalen Objekts anzusprechen – bei clientseitigem JavaScript lautet das globale Objekt üblicherweise window.

Gehen wir von folgendem Codebeispiel im globalen Ausführungskontext aus:

var foo = 10;

function bar() {} // Funktionsdeklaration
(function baz() {}); // Funktionsausdruck

console.log(
  this.foo == foo, // true
  window.bar == bar // true
);

console.log(baz); // ReferenceError, "baz" is not defined

Dieser Code erzeugt folgende Eigenschaften beim Variablenobjekt des globalen Kontexts:

Abbildung 7. Das globale Variablenobjekt.
(VO = Variablenobjekt)

Hier zeigt sich, dass die Funktion baz nicht im Variablenobjekt gespeichert wird, weil sie als Funktionsausdruck notiert wurde. Daher tritt ein ReferenceError auf beim Versuch, außerhalb der Funktion auf baz zuzugreifen.

Anders als in anderen Programmiersprachen wie etwa C oder C++ sind in ECMAScript Funktionen die einzige Möglichkeit, um neue Variablen-Gültigkeitsbereiche (Scopes) zu erzeugen. Variablen und verschachtelte Funktionen, die innerhalb eines solchen Funktions-Gültigkeitsbereiches angelegt werden, sind nach außen hin nicht sichtbar und »verschmutzen« das globalen Variablenobjekt nicht.

Bei der Verwendung von eval wird ebenfalls ein neuer Ausführungskontext erzeugt. Allerdings besitzt dieser Kontext kein eigenes Variablenobjekt, sondern verwendet entweder das globale Variablenobjekt oder das Variablenobjekt des Kontexts, von dem aus eval aufgerufen wurde.

Nun zu den Funktionen und dessen Variablenobjekten. In einem Funktionskontext liegt das Variablenobjekt in Form des Aktivierungsobjekts vor.

Das Aktivierungsobjekt

Wenn eine Funktion aus einem Kontext heraus aktiviert (aufgerufen) wird, dann wird ein spezielles Objekt erzeugt, das Aktivierungsobjekt (activation object). Dieses enthält die formalen Parameter sowie das arguments-Objekt, welches alle, auch nicht deklarierte Parameter unter numerischen Indizes speichert. Das Aktivierungsobjekt wird zudem als Variablenobjekt des Funktionskontexts verwendet.

Das Variablenobjekt einer Funktion ist zunächst ein gewöhnliches Variablenobjekt, wie wir es bereits kennengelernt haben. Es speichert neben den lokalen Variablen und Funktionsdeklarationen zusätzlich noch die formalen Parameter und das arguments-Objekt. Daher bekommt es den eigenen Namen »Aktivierungsobjekt«.

Ein Beispiel:

function foo(x, y) {
  var z = 30;
  function bar() {} // Funktionsdeklaration
  (function baz() {}); // Funktionsausdruck
}

foo(10, 20);

Das Aktivierungsobjekt des foo-Funktionskontexts enthält folgende Daten:

Abbildung 8. Ein Aktivierungsobjekt.

Wieder ist der Funktionsausdruck baz nicht im Variablen- bzw. Aktivierungsobjekt gespeichert.

Die vollständige Beschreibung dieses Themas mit allen Sonderfällen wie dem »Hochziehen« (hoisting) von Variablen- und Funktionsdeklarationen findet sich unter Chapter 2. Variable object.

Kommen wir zum nächsten Teil. Es ist bekannt, dass wir in ECMAScript verschachtelte Funktionen notieren und darin auf die Variablen der äußeren Funktion sowie auf die Variablen des globalen Kontexts zugreifen können. Das besagte Variablenobjekt stellt den Gültigkeitsbereich eines Kontexts dar. Analog zur oben beschriebenen Prototyp-Kette existiert eine Kette von Gültigkeitsbereichen.

Die Scope-Chain (Kette von Gültigkeitsbereichen)

Die Kette von Gültigkeitsbereichen (Scope-Chain) ist eine Liste von Objekten, die nacheinander durchsucht werden, wenn im Code Bezeichner (identifiers) angetroffen werden. Dieser Vorgang nennt sich Auflösung von Bezeichnern (identifier resolution).

Die Regel ist abermals einfach und gleicht der der Prototype-Kette: Wenn eine Variable nicht im eigenen Gültigkeitsbereich (dem Variablen- bzw. Aktivierungsobjekt) gefunden wird, wird beim Variablenobjekt des übergeordneten Kontexts gesucht.

Bezeichner sind Namen von Variablen, Funktionsdeklarationen, formalen Parametern usw. Wenn eine Funktion einen Bezeichner enthält, der weder auf eine lokale Variable, noch eine lokale Funktion oder einen formalen Parameter verweist, so wird diese Variable eine freie Variable genannt. Und um diese freien Variablen zu Objekten aufzulösen, wird die Kette von Gültigkeitsbereichen (Scope-Chain) verwendet.

Im allgemeinen Fall ist diese Kette eine Liste mit allen übergeordneten Variablenobjekten. Hinzu kommt ganz am Anfang der Kette das Variablen- bzw. Aktivierungsobjekt der Funktion selbst. Die Kette kann allerdings auch andere Objekte enthalten, welche dynamisch bei der Ausführung des Kontexts eingefügt wurden. Das passiert bei der Verwendung von with oder try/catch.

Wenn ein Bezeichner aufgelöst wird, so wird die Scope-Chain ausgehend vom Aktivierungsobjekt durchsucht. Wenn der Bezeichner dort nicht gefunden wird, so wird beim nächsten Objekt in der Kette gesucht. Das geht so weiter, bis das oberste Objekt in der Kette erreicht wird – genau wie bei der Prototyp-Ketten.

var x = 10;

(function foo() {
  var y = 20;
  (function bar() {
    var z = 30;
    // »x« und »y« sind »freie Variablen« und finden sich in der Scope-Chain
    // von »bar« in demjenigen Objekt, das dem Aktivierungsobjekt von »bar« folgt
    console.log(x + y + z);
  })();
})();

Wir können uns die Verlinkung zwischen den Objekten in der Scope-Chain als eine interne Eigenschaft __parent__ vorstellen, welche auf das nächste Objekt in der Kette verweist. Dieser Ansatz kann mit dem Rhino-Interpreter getestet werden. Eben dieser Ansatz wird auch in ECMAScript 5 für sogenannte lexikalische Umgebungen (lexical environments) verwendet. Eine weitere Veranschaulichung der Scope-Chain wäre ein einfacher Array. Mithilfe des __parent__-Konzepts und mit dem Wissen, dass übergeordnete Variablenobjekte in der internen Funktionseigenschaft [[Scope]] gespeichert werden, können wir das obige Codebeispiel veranschaulichen:

Abbildung 9. Eine Scope-Chain (Kette von Gültigkeitsbereichen).
(AO = Aktivierungsobjekt, VO = Variablenobjekt)

Während der Ausführung des Codes kann die Scope-Chain mithilfe der with-Anweisung und dem catch-Abschnitt um Objekte erweitert werden. Diese Objekte haben wie alle Objekte Prototypen und Prototyp-Ketten. Dieser Umstand führt uns zu dazu, dass das Nachschlagen eines Bezeichners in der Scope-Chain tatsächlich zweidimensional verläuft: 1. Folge dem Verweis auf den übergeordneten Gültigkeitsbereich, 2. durchlaufe die Prototyp-Kette eines jeden Objekts in der Scope-Chain.

Beispielsweise:

Object.prototype.x = 10;

var w = 20;
var y = 30;

// Im SpiderMonkey, der JavaScript-Interpreter in Firefox, erbt das globale Objekt,
// das ist das Variablenobjekt des globalen Gültigkeitsbereichs, von
// »Objekt.prototype«. Daher können wir auf »x« zugreifen, obwohl eine globale
// Variable dieses Namens nicht definiert wurde. Allerdings wird »x« in der
// Prototyp-Kette des globalen Objekts gefunden.

console.log(x); // 10

(function foo() {

  // Lokale Variablen der Funktion »foo«
  var x = 100;
  var w = 40;

  // »x« wird in »Object.prototype« gefunden, weil {z: 50} davon erbt:

  with ({z: 50}) {
    console.log(w, x, y , z); // 40, 10, 30, 50
  }

  // Nachdem das »with«-Objekt wieder aus der Scope-Chain entfernt wurde,
  // wird »x« wieder im Aktivierungsobjekt des »foo«-Funktionskontexts gefunden.
  // Die Variable »w« ist ebenfalls eine lokale.

  console.log(x, w); // 100, 40

  // Auf die folgende Weise können wir auf die verdeckte globale Variable »w«
  // in der Host-Umgebung des Browsers zugreifen:
  console.log(window.w); // 20

})();

Wir haben es also mit folgender Struktur zu tun. Bevor dem __parent__-Verweis gefolgt wird, wird erst die __proto__-Kette abgearbeitet:

Abbildung 10. Eine durch »with« erweiterte Scope-Chain.
(AO = Aktivierungsobjekt, VO = Variablenobjekt)

Nicht in allen ECMAScript-Interpretern erbt das globale Objekt von Object.prototype. Das in der Abbildung gezeigte Verhalten, dass der globale Kontext auf die »nicht definierte« Variable x verweist, kann im SpiderMonkey (u.a. Firefox) nachvollzogen werden.

Solange alle übergeordneten Variablenobjekte existieren, ist es simpel, aus einer verschachtelten Funktion auf die übergeordneten Daten zuzugreifen: Wir durchlaufen einfach die Scope-Chain, um eine Variable aufzulösen. Allerdings wird ein Kontext mit all seinen Zuständen zerstört, nachdem er die Ausführung seines Codes beendet wurde. Gleichzeitig kann eine verschachtelte Funktion aus seiner übergeordneten äußeren Funktion zurückgegeben und somit nach außen gerettet werden. Zudem kann diese zurückgegebene Funktion später aus einem anderen Kontext heraus aufgerufen werden. Die Frage ist also: Was passiert bei einem solchen Aufruf, wenn doch der Kontext mitsamt freien Variablen bereits verloren ist? Ein allgemeines Konzept, um dieses Problem zu lösen, ist das der (lexikalischen) Closure. Es ist in ECMAScript direkt mit dem Konzept der Scope-Chain verbunden.

Closures

In ECMAScript sind Funktionen Objekte »erster Klasse«. Das bedeutet, dass Funktionen als Parameter an andere Funktionen übergeben werden können. Solche Funktionen werden funktionale Parameter genannt (functional arguments, kurz funargs). Funktionen, deren Aufruf wiederum mit funktionalen Parametern erfolgt, werden Funktionen höherer Ordnung bzw. – in Anlehnung an die Mathematik – Operatoren genannt. Zudem können Funktionen von anderen Funktionen als Ergebnis zurückgegeben werden. Funktionen, die andere zurückgeben, werden Funktionen mit funktionalem Wert genannt (function valued functions oder functions with functional value).

Es gibt zwei konzeptionelle Probleme bei funktionalen Parametern und funktionalen Rückgabewerten. Diese sind Unterprobleme des allgemeinen »Funarg-Problems« (das Problem eines funktionalen Parameters). Um dieses Problem vollständig zu lösen, wurde das Konzept der Closures erfunden – auf Deutsch oft Funktionsabschluss genannt. Betrachten wir diese beiden Unterprobleme genauer. Wir werden sehen, das beide in ECMAScript mithilfe der Eigenschaft [[Scope]] gelöst werden, welche bereits in den Schaubildern vorgekommen ist.

Der erste Untertyp des Funarg-Problems ist das Aufwärts-Problem. Es tritt auf, wenn eine Funktion aus einer anderen nach »oben« gegeben wird und auf freie Variablen zugreift (diesen Begriff haben wir bereits kennengelernt). Damit eine solche innere Funktion auf die Variablen seines übergeordneten Kontexts zugreifen kann, auch nachdem dieser Kontext bereits beendet wurde, speichert die innere Funktion im Moment ihrer Erzeugung die Scope-Chain des übergeordneten Kontext in seiner Eigenschaft [[Scope]]. Wenn diese Funktion nun aufgerufen wird, besteht dessen Scope-Chain aus einer Kombination des Aktivierungsobjekts und eben jener Eigenschaft [[Scope]] – das haben wir im Grunde schon in den obigen Schaubildern gesehen:

Scope-Chain = Aktivierungsobjekt + [[Scope]]

Noch einmal der entscheidende Punkt: Im Moment der Erzeugung speichert eine Funktion die Scope-Chain ihres übergeordneten Kontexts. Diese gespeicherte Scope-Chain wird zum Zeitpunkt der Ausführung zur Auflösung von Variablen verwendet.

function foo() {
  var x = 10;
  return function bar() {
    console.log(x);
  };
}

// »foo« gibt eine Funktion zurück und diese verwendet die freie Variable »x«
var returnedFunction = foo();

// Eine globale Variable namens »x«
var x = 20;

// Die zurückgegebene Funktion wird aufgerufen
returnedFunction(); // ergibt 10, nicht 20

Diese Art des Gültigkeitsbereichs wird statischer oder lexikalischer Gültigkeitsbereich (lexical scope) genannt. Es zeigt sich, dass die Variable x im gespeicherten [[Scope]] der zurückgegebenen Funktion bar gefunden wird. In der Theorie gibt es auch einen dynamischen Gültigkeitsbereich (dynamic scope). Damit würde die Variable x im Beispiel zu 20 aufgelöst werden, nicht zu 10. Allerdings wird ein dynamischer Gültigkeitsbereich in ECMAScript nicht verwendet.

Der zweite Untertyp des Funargs-Problem ist das Abwärts-Problem. In diesem Fall existiert zwar ein übergeordneter Kontext, allerdings liegt eine Mehrdeutigkeit beim Auflösen eines Bezeichners vor. Die Frage ist, aus welchem Gültigkeitsbereich der zu einem Bezeichner passende Wert genommen wird: Aus dem statischen, der bei der Erzeugung der Funktion gespeichert wurde, oder aus dem dynamischen bei der Ausführung (also dem Gültigkeitsbereich des aufrufenden Kontexts)? Um diese Unklarheit zu beseitigen und eine Closure erzeugen zu können, entschied man sich für einen statischen Gültigkeitsbereich:

// Globale Variable »x«
var x = 10;

// Globale Funktion »foo«
function foo() {
  console.log(x);
}

(function (funArg) {

  // Lokale Variable »x«
  var x = 20;

  // Es gibt hier keine Mehrdeutigkeit von »x«. Es wird das globale »x«
  // verwendet, welches im statischen [[Scope]] der Funktion »foo«
  // gespeichert wurde, und nicht etwa das »x« aus dem Kontext,
  // aus dem »funArg« aufgerufen wird.
  funArg(); // 10, nicht 20

})(foo); // übergib »foo« als Parameter »funarg« abwärts in eine Funktion hinein

Zusammengefasst ist ein statischer Gültigkeitsbereich ein unverzichtbares Erfordernis, damit Closures in einer Sprache möglich sind. Allerdings bieten manche Programmiersprachen eine Kombination aus dynamischem und statischem Gültigkeitsbereich, sodass sie dem Programmierer die Wahl lassen, ob Closures erzeugt werden. Da ECMAScript ausschließlich auf statische Gültigkeitsbereiche als Lösung beider Funarg-Probleme setzt, lautet das Fazit: ECMAScript hat eine vollständige Unterstützung von Closures, die intern mit der Eigenschaft [[Scope]] von Funktionen realisiert wird. Nun können wir eine korrekte Definition einer Closure geben:

Eine Closure ist eine Kombination aus einen Codeblock (in ECMAScript ist das eine Funktion) und sämtlichen übergeordneten Gültigkeitsbereichen, die statisch bzw. lexikalisch gespeichert werden. Eine Funktion kann mittels dieser gespeicherten Gültigkeitsbereiche einfach auf freie Variablen zugreifen.

Übrigens sind theoretisch gesehen alle Funktionen in ECMAScript Closures, denn bei jeder normalen Funktion wird die [[Scope]]-Eigenschaft bei der Erzeugung gefüllt.

Ein weiterer wichtiger Punkt ist, dass verschiedene Funktionen denselben übergeordneten Gültigkeitsbereich haben können. Das ist eine ziemlich häufige Situation, z.B. wenn es zwei globale oder verschachtelte Funktionen gibt. In diesem Fall werden die in der [[Scope]]-Eigenschaft gespeicherten Variablen zwischen allen Funktionen geteilt, die dieselbe übergeordnete Scope-Chain besitzen. Wenn Änderungen an diesen Variablen aus einer Closure heraus vorgenommen werden, so wirkt sich dies beim Auslesen der Variablen aus einer anderen Closure aus:

function baz() {
  var x = 1;
  return {
    foo: function foo() { return ++x; },
    bar: function bar() { return --x; }
  };
}

var closures = baz();

console.log(
  closures.foo(), // 2
  closures.bar()  // 1
);

Dieser Code kann folgendermaßen veranschaulicht werden:

Abbildung 11. Ein gemeinsamer [[Scope]].
(AO = Aktivierungsobjekt, VO = Variablenobjekt)

Aus diesem Verhalten resultiert das Problem, das beim Erzeugen von Funktionen in einer Schleife auftritt. Viele Programmierer stoßen auf ein unerwartetes Verhalten, wenn sie den Schleifenzähler in den erzeugten Funktionen verwenden. In diesem Fall haben alle Funktionen Zugriff auf denselben Zählerwert. Es sollte nun klar geworden sein, warum dies so ist: Nämlich weil all diese Funktionen denselben [[Scope]] besitzen, in welchem der Schleifenzähler den letzten ihm zugewiesenen Wert besitzt.

var data = [];

for (var k = 0; k < 3; k++) {
  data[k] = function () {
    alert(k);
  };
}

data[0](); // 3, nicht 0
data[1](); // 3, nicht 1
data[2](); // 3, nicht 2

Es gibt verschiedene Möglichkeiten, diesen Fallstrick zu umgehen. Eine mögliche Lösung ist das Einfügen eines zusätzlichen Objekts in die Scope-Chain, beispielsweise mithilfe einer zusätzlichen Funktion:

var data = [];

for (var k = 0; k < 3; k++) {
  data[k] = (function (x) {
    return function () {
      alert(x);
    };
  })(k); // übergib den Wert »k«
}

// Nun stimmen die Ausgaben
data[0](); // 0
data[1](); // 1
data[2](); // 2

Wer näher an der Theorie der Closures sowie ihrer praktischen Anwendung interessiert ist, wird in Chapter 6. Closures fündig. Mehr über die Scope-Chain finden Sie in Chapter 4. Scope chain.

Im folgenden Abschnitt beschäftigen wir uns mit der letzten Eigenschaft eines Ausführungskontexts: dem this-Wert.

Der Wert this

Der Wert this ist ein besonderes Objekt, das mit dem Ausführungskontext verbunden ist. Daher kann es auch Kontextobjekt genannt werden.

Jedes beliebige Objekt kann als this-Wert eines Kontexts dienen. Ich möchte Missverständnissen entgegentreten, die sich manchmal in Beschreibungen des Ausführungskontexts in ECMAScript und insbesondere des this-Werts finden. Oft wird der this-Wert fälschlicherweise als eine Eigenschaft des Variablenobjekts beschrieben – vor kurzem erst in diesem Buch (auch wenn das verlinkte Kapitel ansonsten sehr gut ist). Zur Wiederholung:

Der Wert this ist eine Eigenschaft des Ausführungskontexts, nicht eine Eigenschaft des Variablenobjekts.

Dieser Unterschied ist entscheidend, denn anders als Variablen unterliegt der this-Wert nicht dem Verfahren der Auflösung von Bezeichnern (identifier resolution). Das heißt, wenn im Code auf this zugegriffen wird, dann wird dessen Wert direkt aus dem Ausführungskontext genommen, ohne die Scope-Chain zu durchsuchen.

Übrigens besitzt Python im Gegensatz zu ECMAScript den Methodenparameter self. Das ist eine einfache Variable, die wie alle Variablen aufgelöst werden und dessen Wert sogar während der Ausführung geändert werden kann. In ECMAScript hingegen ist es nicht möglich, this einen neuen Wert zuzuweisen, weil es wie gesagt keine Variable ist und nicht im Variablenbjekt gespeichert wird.

Im globalen Kontext verweist this auf das globale Objekt selbst. In diesem Fall ist der this-Wert identisch mit dem Variablenobjekt:

var x = 10;

console.log(
  x, // 10
  this.x, // 10
  window.x // 10
);

In einem Funktionskontext kann sich der this-Wert bei jedem Funktionsaufruf unterscheiden. Hier wird der this-Wert vom aufrufenden Kontext bereitgestellt, und zwar über die Art des Aufruf-Ausdrucks (call expression) – das ist die Weise, wie die Funktion aufgerufen wird. Im Beispiel wird die Funktion foo (der Aufgerufene) vom globalen Kontext aufgerufen (der Aufrufende). Anhand des Beispiels sehen wir, wie sich der this-Wert bei gleichbleibendem Funktionscode abhängig von der Art des Aufrufs unterscheidet.

// Der Code der Funktion »foo« ändert sich nicht, aber der this-Wert
// unterscheidet sich bei jedem Aufruf

function foo() {
  alert(this);
}

// Der Aufrufer aktiviert »foo« und stellt den this-Wert bereit

foo(); // this ist das globale Objekt
foo.prototype.constructor(); // this ist foo.prototype

var bar = {
  baz: foo
};

bar.baz(); // this ist bar

(bar.baz)(); // this ist hier ebenfalls bar
(bar.baz = bar.baz)(); // hier wiederum das globale Objekt
(bar.baz, bar.baz)(); // ebenfalls das globale Objekt
(false || bar.baz)(); // ebenfalls das globale Objekt

var otherFoo = bar.baz;
otherFoo(); // this ist wieder das globale Objekt

Um genauer zu verstehen, warum und vor allem wie der this-Wert vom Funktionsaufruf abhängt, können Sie Chapter 3. This lesen, in dem die genannten Fälle detailliert besprochen werden.

Fazit

An dieser Stelle sind wir am Ende dieser kurzen Übersicht angelangt – auch wenn sie letztendlich nicht kurz wurde. ;) Trotzdem würde die eingehende Erläuterung dieser Themen ein ganzes Buch erfordern. Wir haben zwei große Themen noch nicht einmal berührt: Funktionen und die verschiedenen Typen von Funktionen (Funktionsausdrücke vs. Funktionsdeklarationen) sowie die Auswertungsstrategie in ECMAScript. Beide Themen werden in gesonderten Kapiteln der Artikelreihe behandelt: Chapter 5. Functions und Chapter 8. Evaluation strategy.

Falls Sie Kommentare, Fragen oder Ergänzungen haben, stehe ich gerne für Diskussionen in den Blog-Kommentaren zur Verfügung.

Viel Erfolg beim Lernen von ECMAScript!

Verfasser: Dmitry A. Soshnikov
Veröffentlicht am: 2010-09-02

Deutsche Übersetzung: Mathias Schäfer (molily), zapperlott@gmail.com. Dank geht an Ingo Chao, Peter Seliger und Axel Wienberg für Feedback und Korrekturen zur Übersetzung.