Kapitel 16. Variablen: Scopes, Umgebungen und Closures
Inhaltsverzeichnis
Das Buch kaufen
(Werbung, bitte nicht blockieren.)

Kapitel 16. Variablen: Scopes, Umgebungen und Closures

Dieses Kapitel erklärt zuerst, wie man Variablen benutzt, und geht dann ins Detail, wie sie funktionieren (Umgebungen, Closures usw.).

Deklarieren einer Variable

In JavaScript deklarieren Sie eine Variable mit einer var-Anweisung, bevor Sie sie verwenden:

var foo;
foo = 3; // OK, has been declared
bar = 5; // not OK, an undeclared variable

Sie können eine Deklaration auch mit einer Zuweisung kombinieren, um eine Variable sofort zu initialisieren

var foo = 3;

Der Wert einer nicht initialisierten Variable ist undefined:

> var x;
> x
undefined

Hintergrund: Statisch versus dynamisch

Sie können die Funktionsweise eines Programms aus zwei Blickwinkeln betrachten:

Statisch (oder lexikalisch)

Sie betrachten das Programm, wie es im Quellcode vorliegt, ohne es auszuführen. Angesichts des folgenden Codes können wir die statische Aussage treffen, dass Funktion g innerhalb von Funktion f verschachtelt ist

function f() {
    function g() {
    }
}

Das Adjektiv lexikalisch wird synonym mit statisch verwendet, da beides auf das Lexikon (die Wörter, den Quellcode) des Programms zutrifft.

Dynamisch

Sie betrachten, was während der Ausführung des Programms passiert („zur Laufzeit“). Angesichts des folgenden Codes

function g() {
}
function f() {
    g();
}

wenn wir f() aufrufen, ruft es g() auf. Während der Laufzeit stellt g, das von f aufgerufen wird, eine dynamische Beziehung dar.

Hintergrund: Der Scope einer Variable

Für den Rest dieses Kapitels sollten Sie die folgenden Konzepte verstehen:

Der Scope einer Variable

Der Scope einer Variable sind die Orte, an denen sie zugänglich ist. Zum Beispiel

function foo() {
    var x;
}

Hier ist der direkte Scope von x die Funktion foo().

Lexikalische Scoping
Variablen in JavaScript sind lexikalisch gesoped, daher bestimmt die statische Struktur eines Programms den Scope einer Variable (er wird nicht von z. B. dem Ort beeinflusst, von dem eine Funktion aufgerufen wird).
Verschachtelte Scopes

Wenn Scopes innerhalb des direkten Scopes einer Variable verschachtelt sind, ist die Variable in all diesen Scopes zugänglich

function foo(arg) {
    function bar() {
        console.log('arg: '+arg);
    }
    bar();
}
console.log(foo('hello')); // arg: hello

Der direkte Scope von arg ist foo(), aber er ist auch im verschachtelten Scope bar() zugänglich. In Bezug auf die Verschachtelung ist foo() der äußere Scope und bar() der innere Scope.

Shadowing

Wenn ein Scope eine Variable deklariert, die denselben Namen wie eine in einem umgebenden Scope hat, wird der Zugriff auf die äußere Variable im inneren Scope und allen darin verschachtelten Scopes blockiert. Änderungen an der inneren Variable beeinflussen die äußere Variable nicht, die wieder zugänglich ist, nachdem der innere Scope verlassen wurde:

var x = "global";
function f() {
    var x = "local";
    console.log(x); // local
}
f();
console.log(x); // global

Innerhalb der Funktion f() wird das globale x durch ein lokales x überschattet.

Variablen sind funktionsbezogen

Die meisten Mainstream-Sprachen sind block-gesoped: Variablen „leben in“ dem innersten umgebenden Codeblock. Hier ist ein Beispiel aus Java:

public static void main(String[] args) {
    { // block starts
        int foo = 4;
    } // block ends
    System.out.println(foo); // Error: cannot find symbol
}

Im vorherigen Code ist die Variable foo nur innerhalb des Blocks zugänglich, der sie direkt umgibt. Wenn wir versuchen, darauf nach dem Ende des Blocks zuzugreifen, erhalten wir einen Kompilierungsfehler.

Im Gegensatz dazu sind JavaScript-Variablen funktions-gesoped: nur Funktionen führen neue Scopes ein; Blöcke werden beim Scoping ignoriert. Zum Beispiel:

function main() {
    { // block starts
        var foo = 4;
    } // block ends
    console.log(foo); // 4
}

Anders ausgedrückt ist foo innerhalb von main() zugänglich, nicht nur innerhalb des Blocks.

Variablendeklarationen werden gehisst

JavaScript hisst alle Variablendeklarationen, d.h. es verschiebt sie an den Anfang ihrer direkten Scopes. Das macht deutlich, was passiert, wenn auf eine Variable zugegriffen wird, bevor sie deklariert wurde:

function f() {
    console.log(bar);  // undefined
    var bar = 'abc';
    console.log(bar);  // abc
}

Wir sehen, dass die Variable bar bereits in der ersten Zeile von f() existiert, aber sie hat noch keinen Wert; das heißt, die Deklaration wurde gehisst, aber nicht die Zuweisung. JavaScript führt f() so aus, als wäre sein Code

function f() {
    var bar;
    console.log(bar);  // undefined
    bar = 'abc';
    console.log(bar);  // abc
}

Wenn Sie eine bereits deklarierte Variable deklarieren, passiert nichts (der Wert der Variable bleibt unverändert)

> var x = 123;
> var x;
> x
123

Jede Funktionsdeklaration wird ebenfalls gehisst, aber auf eine etwas andere Weise. Die vollständige Funktion wird gehisst, nicht nur die Erstellung der Variable, in der sie gespeichert ist (siehe Hoisting).

Best Practice: Seien Sie sich des Hissings bewusst, aber haben Sie keine Angst davor

Einige JavaScript-Styleguides empfehlen, Variablendeklarationen nur am Anfang einer Funktion zu platzieren, um nicht vom Hissing getäuscht zu werden. Wenn Ihre Funktion relativ klein ist (was sie ohnehin sein sollte), können Sie es sich leisten, diese Regel etwas zu lockern und Variablen nahe dort zu deklarieren, wo sie verwendet werden (z. B. innerhalb einer for-Schleife). Das kapselt Codeabschnitte besser. Offensichtlich sollten Sie sich bewusst sein, dass diese Kapselung nur konzeptionell ist, da das Funktions-weite Hissing immer noch stattfindet.

Einführung eines neuen Scopes über eine IIFE

Sie führen typischerweise einen neuen Scope ein, um die Lebensdauer einer Variable einzuschränken. Ein Beispiel, wo Sie dies tun möchten, ist der „dann“-Teil einer if-Anweisung: Er wird nur ausgeführt, wenn die Bedingung erfüllt ist; und wenn er ausschließlich Hilfsvariablen verwendet, möchten wir nicht, dass diese in den umgebenden Scope „auslaufen“:

function f() {
    if (condition) {
        var tmp = ...;
        ...
    }
    // tmp still exists here
    // => not what we want
}

Wenn Sie einen neuen Scope für den then-Block einführen möchten, können Sie eine Funktion definieren und sie sofort aufrufen. Dies ist ein Workaround, eine Simulation von Block-Scoping:

function f() {
    if (condition) {
        (function () {  // open block
            var tmp = ...;
            ...
        }());  // close block
    }
}

Dies ist ein gängiges Muster in JavaScript. Ben Alman schlug vor, es immediately invoked function expression (IIFE, ausgesprochen „iffy“) zu nennen. Im Allgemeinen sieht eine IIFE so aus

(function () { // open IIFE
    // inside IIFE
}()); // close IIFE

Hier sind einige Dinge, die Sie über eine IIFE beachten sollten

Sie wird sofort aufgerufen
Die Klammern nach der schließenden geschweiften Klammer der Funktion rufen sie sofort auf. Das bedeutet, dass ihr Körper sofort ausgeführt wird.
Es muss ein Ausdruck sein
Wenn eine Anweisung mit dem Schlüsselwort function beginnt, erwartet der Parser, dass es sich um eine Funktionsdeklaration handelt (siehe Ausdrücke versus Anweisungen). Aber eine Funktionsdeklaration kann nicht sofort aufgerufen werden. Daher teilen wir dem Parser mit, dass das Schlüsselwort function der Anfang eines Funktionsausdrucks ist, indem wir die Anweisung mit einer öffnenden Klammer beginnen. Innerhalb von Klammern kann nur das stehen, was ein Ausdruck ist.
Das abschließende Semikolon ist erforderlich

Wenn Sie es zwischen zwei IIFEs vergessen, funktioniert Ihr Code nicht mehr:

(function () {
    ...
}()) // no semicolon
(function () {
    ...
}());

Der vorherige Code wird als Funktionsaufruf interpretiert – die erste IIFE (einschließlich der Klammern) ist die aufzurufende Funktion, und die zweite IIFE ist der Parameter.

Hinweis

Eine IIFE verursacht Kosten (sowohl kognitiv als auch in Bezug auf die Leistung), daher ist es selten sinnvoll, sie innerhalb einer if-Anweisung zu verwenden. Das vorherige Beispiel wurde aus didaktischen Gründen gewählt.

IIFE-Variante: Präfixoperatoren

Sie können den Ausdruckskontext auch über Präfixoperatoren erzwingen. Zum Beispiel können Sie dies über den logischen NICHT-Operator tun:

!function () { // open IIFE
    // inside IIFE
}(); // close IIFE

oder über den void-Operator (siehe Der void-Operator)

void function () { // open IIFE
    // inside IIFE
}(); // close IIFE

Der Vorteil der Verwendung von Präfixoperatoren ist, dass das Vergessen des abschließenden Semikolons keine Probleme verursacht.

IIFE-Variante: Bereits im Ausdruckskontext

Beachten Sie, dass die Erzwingung des Ausdrucks Kontext für eine IIFE nicht notwendig ist, wenn Sie sich bereits im Ausdruckskontext befinden. Dann benötigen Sie weder Klammern noch Präfixoperatoren. Zum Beispiel:

var File = function () { // open IIFE
    var UNTITLED = 'Untitled';
    function File(name) {
        this.name = name || UNTITLED;
    }
    return File;
}(); // close IIFE

Im vorherigen Beispiel gibt es zwei verschiedene Variablen, die den Namen File tragen. Einerseits gibt es die Funktion, die nur direkt innerhalb der IIFE zugänglich ist. Andererseits gibt es die Variable, die in der ersten Zeile deklariert wird. Ihr wird der Wert zugewiesen, der in der IIFE zurückgegeben wird.

IIFE-Variante: Eine IIFE mit Parametern

Sie können Parameter verwenden, um Variablen für das Innere der IIFE zu definieren:

var x = 23;
(function (twice) {
    console.log(twice);
}(x * 2));

Dies ähnelt

var x = 23;
(function () {
    var twice = x * 2;
    console.log(twice);
}());

IIFE-Anwendungen

Eine IIFE ermöglicht es Ihnen, private Daten an eine Funktion anzuhängen. Dann müssen Sie keine globale Variable deklarieren und können die Funktion straff mit ihrem Zustand verpacken. Sie vermeiden, den globalen Namensraum zu verunreinigen:

var setValue = function () {
    var prevValue;
    return function (value) { // define setValue
        if (value !== prevValue) {
            console.log('Changed: ' + value);
            prevValue = value;
        }
    };
}();

Andere Anwendungen von IIFEs werden an anderer Stelle in diesem Buch erwähnt

Globale Variablen

Der Scope, der ein gesamtes Programm umfasst, wird als globaler Scope oder Programm-Scope bezeichnet. Dies ist der Scope, in dem Sie sich befinden, wenn Sie ein Skript betreten (sei es ein <script>-Tag auf einer Webseite oder eine .js-Datei). Innerhalb des globalen Scopes können Sie einen verschachtelten Scope erstellen, indem Sie eine Funktion definieren. Innerhalb einer solchen Funktion können Sie wiederum Scopes verschachteln. Jeder Scope hat Zugriff auf seine eigenen Variablen und auf die Variablen in den umgebenden Scopes. Da der globale Scope alle anderen Scopes umgibt, sind seine Variablen überall zugänglich:

// here we are in global scope
var globalVariable = 'xyz';
function f() {
    var localVariable = true;
    function g() {
        var anotherLocalVariable = 123;

        // All variables of surround scopes are accessible
        localVariable = false;
        globalVariable = 'abc';
    }
}
// here we are again in global scope

Best Practice: Vermeiden Sie die Erstellung globaler Variablen

Globale Variablen haben zwei Nachteile. Erstens sind Softwareteile, die auf globale Variablen angewiesen sind, anfällig für Nebeneffekte; sie sind weniger robust, verhalten sich unvorhersehbarer und sind weniger wiederverwendbar.

Zweitens teilen sich alle JavaScripts auf einer Webseite dieselben globalen Variablen: Ihr Code, eingebaute Funktionen, Analyse-Code, Social-Media-Buttons usw. Das bedeutet, dass Namenskonflikte zu einem Problem werden können. Deshalb ist es am besten, so viele Variablen wie möglich vor dem globalen Scope zu verbergen. Tun Sie zum Beispiel nicht das hier

<!-- Don’t do this -->
<script>
    // Global scope
    var tmp = generateData();
    processData(tmp);
    persistData(tmp);
</script>

Die Variable tmp wird global, da ihre Deklaration im globalen Scope ausgeführt wird. Aber sie wird nur lokal verwendet. Daher können wir eine IIFE (siehe Einführung eines neuen Scopes über eine IIFE) verwenden, um sie innerhalb eines verschachtelten Scopes zu verbergen

<script>
    (function () {  // open IIFE
        // Local scope
        var tmp = generateData();
        processData(tmp);
        persistData(tmp);
    }());  // close IIFE
</script>

Modulsysteme führen zu weniger globalen Variablen

Glücklicherweise eliminieren Modulsysteme (siehe Modulsysteme) das Problem globaler Variablen größtenteils, da Module nicht über den globalen Scope kommunizieren und da jedes Modul seinen eigenen Scope für modul-globale Variablen hat.

Das globale Objekt

Die ECMAScript-Spezifikation verwendet die interne Datenstruktur Umgebung, um Variablen zu speichern (siehe Umgebungen: Verwaltung von Variablen). Die Sprache hat die etwas ungewöhnliche Eigenschaft, die Umgebung für globale Variablen über ein Objekt zugänglich zu machen, das sogenannte globale Objekt. Das globale Objekt kann verwendet werden, um globale Variablen zu erstellen, zu lesen und zu ändern. Im globalen Scope zeigt this darauf:

> var foo = 'hello';
> this.foo  // read global variable
'hello'

> this.bar = 'world';  // create global variable
> bar
'world'

Beachten Sie, dass das globale Objekt Prototypen hat. Wenn Sie alle seine (eigenen und geerbten) Eigenschaften auflisten möchten, benötigen Sie eine Funktion wie getAllPropertyNames() aus Auflistung aller Eigenschaftenschlüssel

> getAllPropertyNames(window).sort().slice(0, 5)
[ 'AnalyserNode', 'Array', 'ArrayBuffer', 'Attr', 'Audio' ]

Der JavaScript-Schöpfer Brendan Eich betrachtet das globale Objekt als einen seiner „größten Bedauern“. Es wirkt sich negativ auf die Leistung aus, erschwert die Implementierung von Variablen-Scoping und führt zu weniger modularem Code.

Plattformübergreifende Überlegungen

Browser und Node.js haben globale Variablen, um auf das globale Objekt zu verweisen. Leider sind sie unterschiedlich:

Auf beiden Plattformen verweist this auf das globale Objekt, aber nur, wenn Sie sich im globalen Scope befinden. Das ist auf Node.js fast nie der Fall. Wenn Sie auf plattformübergreifende Weise auf das globale Objekt zugreifen möchten, können Sie ein Muster wie das folgende verwenden

(function (glob) {
    // glob points to global object
}(typeof window !== 'undefined' ? window : global));

Von nun an verwende ich window, um mich auf das globale Objekt zu beziehen, aber in plattformübergreifendem Code sollten Sie das vorherige Muster und glob anstelle davon verwenden.

Anwendungsfälle für window

Dieser Abschnitt beschreibt Anwendungsfälle für den Zugriff auf globale Variablen über window. Aber die allgemeine Regel lautet: Vermeiden Sie das so weit wie möglich.

Anwendungsfall: Markieren globaler Variablen

Das Präfix window ist ein visueller Hinweis darauf, dass Code sich auf eine globale Variable und nicht auf eine lokale bezieht:

var foo = 123;
(function () {
    console.log(window.foo);  // 123
}());

Dies macht Ihren Code jedoch brüchig. Er funktioniert nicht mehr, sobald Sie foo aus dem globalen Scope in einen anderen umgebenden Scope verschieben

(function () {
    var foo = 123;
    console.log(window.foo);  // undefined
}());

Daher ist es besser, foo als Variable und nicht als Eigenschaft von window zu referenzieren. Wenn Sie deutlich machen wollen, dass foo eine globale oder global-ähnliche Variable ist, können Sie einen Namenspräfix wie g_ hinzufügen

var g_foo = 123;
(function () {
    console.log(g_foo);
}());

Anwendungsfall: Built-ins

Ich bevorzuge es, eingebaute globale Variablen nicht über window zu referenzieren. Sie sind bekannte Namen, daher gewinnen Sie wenig durch einen Hinweis darauf, dass sie global sind. Und das vorangestellte window fügt Unordnung hinzu:

window.isNaN(...)  // no
isNaN(...)  // yes

Anwendungsfall: Style-Checker

Wenn Sie mit einem Style-Checking-Tool wie JSLint und JSHint arbeiten, bedeutet die Verwendung von window, dass Sie keinen Fehler erhalten, wenn Sie auf eine globale Variable verweisen, die in der aktuellen Datei nicht deklariert ist. Beide Tools bieten jedoch Möglichkeiten, ihnen solche Variablen mitzuteilen und solche Fehler zu verhindern (suchen Sie nach „global variable“ in ihrer Dokumentation).

Anwendungsfall: Überprüfen, ob eine globale Variable existiert

Es ist kein häufiger Anwendungsfall, aber Shims und Polyfills (siehe Shims versus Polyfills) müssen insbesondere überprüfen, ob eine globale Variable someVariable existiert. In diesem Fall hilft window:

if (window.someVariable) { ... }

Dies ist eine sichere Methode, um diese Prüfung durchzuführen. Die folgende Anweisung wirft eine Ausnahme, wenn someVariable nicht deklariert wurde

// Don’t do this
if (someVariable) { ... }

Es gibt zwei zusätzliche Möglichkeiten, wie Sie über window prüfen können; sie sind grob äquivalent, aber etwas expliziter

if (window.someVariable !== undefined) { ... }
if ('someVariable' in window) { ... }

Die allgemeine Methode zur Überprüfung, ob eine Variable existiert (und einen Wert hat) ist über typeof (siehe typeof: Kategorisierung von Primitiven)

if (typeof someVariable !== 'undefined') { ... }

Anwendungsfall: Erstellen von Dingen im globalen Scope

window ermöglicht es Ihnen, Dinge zum globalen Scope hinzuzufügen (auch wenn Sie sich in einem verschachtelten Scope befinden), und es ermöglicht Ihnen, dies bedingt zu tun:

if (!window.someApiFunction) {
    window.someApiFunction = ...;
}

Es ist normalerweise am besten, Dinge über var zum globalen Scope hinzuzufügen, während Sie sich im globalen Scope befinden. window bietet jedoch eine saubere Möglichkeit, bedingt Hinzufügungen vorzunehmen.

Umgebungen: Verwaltung von Variablen

Variablen entstehen, wenn die Programmausführung ihre Scopes betritt. Dann benötigen sie Speicherplatz. Die Datenstruktur, die diesen Speicherplatz bereitstellt, wird in JavaScript als Umgebung bezeichnet. Sie ordnet Variablennamen Werten zu. Ihre Struktur ähnelt sehr der von JavaScript-Objekten. Umgebungen leben manchmal weiter, nachdem man ihren Scope verlassen hat. Daher werden sie auf einem Heap und nicht auf einem Stack gespeichert.

Variablen werden auf zwei Arten weitergegeben. Sie haben, wenn man so will, zwei Dimensionen

Dynamische Dimension: Aufrufen von Funktionen

Jedes Mal, wenn eine Funktion aufgerufen wird, benötigt sie neuen Speicher für ihre Parameter und Variablen. Nachdem sie beendet ist, kann dieser Speicher normalerweise wieder freigegeben werden. Nehmen Sie als Beispiel die folgende Implementierung der Fakultätsfunktion. Sie ruft sich rekursiv mehrmals selbst auf und jedes Mal benötigt sie neuen Speicher für n:

function fac(n) {
    if (n <= 1) {
        return 1;
    }
    return n * fac(n - 1);
}
Lexikalische (statische) Dimension: Verbindung zu den umgebenden Scopes behalten

Unabhängig davon, wie oft eine Funktion aufgerufen wird, benötigt sie immer Zugriff auf ihre eigenen (neuen) lokalen Variablen sowie auf die Variablen der umgebenden Scopes. Zum Beispiel hat die folgende Funktion, doNTimes, eine Hilfsfunktion, doNTimesRec, darin. Wenn doNTimesRec sich mehrmals selbst aufruft, wird jedes Mal eine neue Umgebung erstellt. doNTimesRec bleibt jedoch während dieser Aufrufe mit der einzigen Umgebung von doNTimes verbunden (ähnlich wie alle Funktionen eine einzige globale Umgebung teilen). doNTimesRec benötigt diese Verbindung, um in Zeile (1) auf action zuzugreifen:

function doNTimes(n, action) {
    function doNTimesRec(x) {
        if (x >= 1) {
            action();  // (1)
            doNTimesRec(x-1);
        }
    }
    doNTimesRec(n);
}

Diese beiden Dimensionen werden wie folgt behandelt

Dynamische Dimension: Stack von Ausführungskontexten
Jedes Mal, wenn eine Funktion aufgerufen wird, wird eine neue Umgebung erstellt, um Bezeichner (von Parametern und Variablen) Werten zuzuordnen. Um die Rekursion zu behandeln, werden Ausführungskontexte – Verweise auf Umgebungen – in einem Stack verwaltet. Dieser Stack spiegelt den Aufrufstack wider.
Lexikalische Dimension: Kette von Umgebungen

Um diese Dimension zu unterstützen, zeichnet eine Funktion den Scope auf, in dem sie erstellt wurde, über die interne Eigenschaft [[Scope]] auf. Wenn eine Funktion aufgerufen wird, wird eine Umgebung für den neuen Scope erstellt, der betreten wird. Diese Umgebung hat ein Feld namens outer, das auf die Umgebung des äußeren Scopes verweist und über [[Scope]] eingerichtet wird. Daher gibt es immer eine Kette von Umgebungen, beginnend mit der aktuell aktiven Umgebung, fortgesetzt mit ihrer äußeren Umgebung und so weiter. Jede Kette endet mit der globalen Umgebung (dem Scope aller ursprünglich aufgerufenen Funktionen). Das Feld outer der globalen Umgebung ist null.

Um einen Bezeichner aufzulösen, wird die vollständige Umgebungskette durchlaufen, beginnend mit der aktiven Umgebung.

Betrachten wir ein Beispiel.

function myFunction(myParam) {
    var myVar = 123;
    return myFloat;
}
var myFloat = 1.3;
// Step 1
myFunction('abc');  // Step 2

Abbildung 16-1 veranschaulicht, was passiert, wenn der vorherige Code ausgeführt wird

  1. myFunction und myFloat wurden in der globalen Umgebung (#0) gespeichert. Beachten Sie, dass das Funktionsobjekt, auf das von myFunction verwiesen wird, über die interne Eigenschaft [[Scope]] auf seinen Scope (den globalen Scope) verweist.
  2. Für die Ausführung von myFunction('abc') wird eine neue Umgebung (#1) erstellt, die den Parameter und die lokale Variable enthält. Sie verweist über outer auf ihre äußere Umgebung (dieselbe wie myFunction.[[Scope]]). Dank der äußeren Umgebung kann myFunction auf myFloat zugreifen.

Closures: Funktionen bleiben mit ihren Geburts-Scopes verbunden

Wenn eine Funktion den Scope verlässt, in dem sie erstellt wurde, bleibt sie mit den Variablen dieses Scopes (und der umgebenden Scopes) verbunden. Zum Beispiel:

function createInc(startValue) {
    return function (step) {
        startValue += step;
        return startValue;
    };
}

Die von createInc() zurückgegebene Funktion verliert nicht ihre Verbindung zu startValue – die Variable liefert der Funktion einen Zustand, der über Funktionsaufrufe hinweg bestehen bleibt

> var inc = createInc(5);
> inc(1)
6
> inc(2)
8

Eine Closure ist eine Funktion plus die Verbindung zum Scope, in dem die Funktion erstellt wurde. Der Name leitet sich davon ab, dass eine Closure die freien Variablen einer Funktion „umschließt“. Eine Variable ist frei, wenn sie nicht innerhalb der Funktion deklariert ist – d. h., wenn sie „von außen“ kommt.

Umgang mit Closures über Umgebungen

Tipp

Dies ist ein fortgeschrittener Abschnitt, der tiefer auf die Funktionsweise von Closures eingeht. Sie sollten mit Umgebungen vertraut sein (siehe Umgebungen: Verwaltung von Variablen).

Eine Closure ist ein Beispiel dafür, dass eine Umgebung nach Verlassen ihres Scopes weiterbesteht. Um zu veranschaulichen, wie Closures funktionieren, untersuchen wir die vorherige Interaktion mit createInc() und teilen sie in vier Schritte auf (während jedes Schritts sind der aktive Ausführungskontext und seine Umgebung hervorgehoben; wenn eine Funktion aktiv ist, ist sie ebenfalls hervorgehoben)

  1. Dieser Schritt findet vor der Interaktion statt und nach der Auswertung der Funktionsdeklaration von createInc. Ein Eintrag für createInc wurde der globalen Umgebung (#0) hinzugefügt und verweist auf ein Funktionsobjekt.

    image with no caption
  2. Dieser Schritt tritt während der Ausführung des Funktionsaufrufs createInc(5) auf. Eine neue Umgebung (#1) für createInc wird erstellt und auf den Stack gelegt. Ihre äußere Umgebung ist die globale Umgebung (dieselbe wie createInc.[[Scope]]). Die Umgebung enthält den Parameter startValue.

    image with no caption
  3. Dieser Schritt geschieht nach der Zuweisung an inc. Nachdem wir von createInc zurückgekehrt sind, wurde der Ausführungskontext, der auf seine Umgebung verweist, vom Stack entfernt, aber die Umgebung existiert weiterhin im Heap, da inc.[[Scope]] darauf verweist. inc ist eine Closure (Funktion plus Geburtsumgebung).

    image with no caption
  4. Dieser Schritt findet während der Ausführung von inc(1) statt. Eine neue Umgebung (#1) wurde erstellt und ein Ausführungskontext, der darauf verweist, wurde auf den Stack gelegt. Ihre äußere Umgebung ist das [[Scope]] von inc. Die äußere Umgebung gibt inc Zugriff auf startValue.

    image with no caption
  5. Dieser Schritt geschieht nach der Ausführung von inc(1). Kein Verweis (Ausführungskontext, outer-Feld oder [[Scope]]) verweist mehr auf die Umgebung von inc. Sie wird daher nicht mehr benötigt und kann aus dem Heap entfernt werden.

    image with no caption

Fallstrick: Versehentliches Teilen einer Umgebung

Manchmal wird das Verhalten von Funktionen, die Sie erstellen, von einer Variable im aktuellen Scope beeinflusst. In JavaScript kann das problematisch sein, da jede Funktion mit dem Wert arbeiten sollte, den die Variable zum Zeitpunkt der Erstellung der Funktion hatte. Aufgrund von Closures arbeitet die Funktion jedoch immer mit dem aktuellen Wert der Variable. In for-Schleifen kann dies dazu führen, dass Dinge nicht richtig funktionieren. Ein Beispiel wird die Sache verdeutlichen:

function f() {
    var result = [];
    for (var i=0; i<3; i++) {
        var func = function () {
            return i;
        };
        result.push(func);
    }
    return result;
}
console.log(f()[1]());  // 3

f gibt ein Array mit drei Funktionen zurück. Alle diese Funktionen können immer noch auf die Umgebung von f und damit auf i zugreifen. Tatsächlich teilen sie sich dieselbe Umgebung. Bedauerlicherweise hat i nach Abschluss der Schleife den Wert 3 in dieser Umgebung. Daher geben alle Funktionen 3 zurück.

Das ist nicht das, was wir wollen. Um die Dinge zu reparieren, müssen wir einen Schnappschuss des Index i erstellen, bevor wir eine Funktion erstellen, die ihn verwendet. Mit anderen Worten, wir wollen jede Funktion mit dem Wert bündeln, den i zum Zeitpunkt der Erstellung der Funktion hatte. Wir gehen daher wie folgt vor:

  1. Erstellen Sie für jede Funktion im zurückgegebenen Array eine neue Umgebung.
  2. Speichern Sie (eine Kopie) des aktuellen Werts von i in dieser Umgebung.

Nur Funktionen erstellen Umgebungen, daher verwenden wir eine IIFE (siehe Introducing a New Scope via an IIFE), um Schritt 1 zu erreichen.

function f() {
    var result = [];
    for (var i=0; i<3; i++) {
        (function () { // step 1: IIFE
            var pos = i; // step 2: copy
            var func = function () {
                return pos;
            };
            result.push(func);
        }());
    }
    return result;
}
console.log(f()[1]());  // 1

Beachten Sie, dass das Beispiel reale Relevanz hat, da ähnliche Szenarien auftreten, wenn Sie Ereignisbehandlungsroutinen über Schleifen zu DOM-Elementen hinzufügen.

Weiter: 17. Objekte und Vererbung