Dieser Inhalt wurde automatisch aus dem Englischen übersetzt, und kann Fehler enthalten.Erfahre mehr über dieses Experiment.
Closures
EinClosure ist die Kombination einer Funktion, die zusammen mit den Referenzen zu ihrem umgebenden Zustand (derlexikalischen Umgebung) gebündelt ist. Mit anderen Worten, ein Closure ermöglicht einer Funktion den Zugriff auf ihren äußeren Scope. In JavaScript werden Closures jedes Mal erstellt, wenn eine Funktion erstellt wird, und zwar zur Zeit der Funktionsdefinition.
In diesem Artikel
Lexikalische Scoping
Betrachten Sie den folgenden Beispielcode:
function init() { var name = "Mozilla"; // name is a local variable created by init function displayName() { // displayName() is the inner function, that forms a closure console.log(name); // use variable declared in the parent function } displayName();}init();init() erstellt eine lokale Variable namensname und eine Funktion namensdisplayName(). DiedisplayName()-Funktion ist eine innere Funktion, die innerhalb voninit() definiert ist und nur innerhalb des Rumpfes derinit()-Funktion zur Verfügung steht. Beachten Sie, dass diedisplayName()-Funktion keine eigenen lokalen Variablen hat. Da innere Funktionen jedoch Zugriff auf die Variablen von äußeren Scopes haben, kanndisplayName() auf die Variablename zugreifen, die in der übergeordneten Funktioninit() deklariert ist.
Wenn Sie diesen Code in Ihrer Konsole ausführen, können Sie sehen, dass dieconsole.log()-Anweisung innerhalb derdisplayName()-Funktion erfolgreich den Wert dername-Variable anzeigt, der in ihrer übergeordneten Funktion deklariert ist. Dies ist ein Beispiel fürlexikalisches Scoping, das beschreibt, wie ein Parser Variablennamen auflöst, wenn Funktionen verschachtelt sind. Das Wortlexikalisch bezieht sich darauf, dass lexikalisches Scoping den Ort verwendet, an dem eine Variable im Quellcode deklariert wird, um zu bestimmen, wo diese Variable verfügbar ist. Verschachtelte Funktionen haben Zugriff auf Variablen, die in ihrem äußeren Scope deklariert sind.
Scoping mit let und const
Traditionell (vor ES6) hatten JavaScript-Variablen nur zwei Arten von Scopes:Funktions-Scope undglobaler Scope. Mitvar deklarierte Variablen sind entweder funktions-gescope oder global-gescope, je nachdem, ob sie innerhalb oder außerhalb einer Funktion deklariert sind. Das kann problematisch sein, da Blöcke mit geschweiften Klammern keine Scopes erstellen:
if (Math.random() > 0.5) { var x = 1;} else { var x = 2;}console.log(x);Für Personen aus anderen Sprachen (z. B. C, Java), in denen Blöcke Scopes erstellen, sollte der obige Code einen Fehler in derconsole.log-Zeile werfen, da wir uns außerhalb des Scopes vonx in einem der Blöcke befinden. Da Blöcke jedoch keine Scopes fürvar erstellen, wird hier tatsächlich eine globale Variable erstellt. Es gibt auchein praktisches Beispiel weiter unten, das veranschaulicht, wie dies in Kombination mit Closures tatsächlich Fehler verursachen kann.
In ES6 führte JavaScript die Deklarationenlet undconst ein, die Ihnen unter anderem erlauben, block-gescopte Variablen zu erstellen, wie beispielsweisetemporale Todeszonen.
if (Math.random() > 0.5) { const x = 1;} else { const x = 2;}console.log(x); // ReferenceError: x is not definedIm Wesentlichen werden Blöcke in ES6 endlich als Scopes behandelt, aber nur, wenn Sie Variablen mitlet oderconst deklarieren. Darüber hinaus führte ES6Module ein, die eine weitere Art von Scope einführten. Closures können Variablen in all diesen Scopes erfassen, die wir später einführen werden.
Closures
Betrachten Sie das folgende Codebeispiel:
function makeFunc() { const name = "Mozilla"; function displayName() { console.log(name); } return displayName;}const myFunc = makeFunc();myFunc();Das Ausführen dieses Codes hat exakt die gleiche Wirkung wie das vorherige Beispiel derinit()-Funktion oben. Der Unterschied (und das Interessante) ist, dass diedisplayName()-innere Funktion aus der äußeren Funktionzurückgegeben wird, bevor sie ausgeführt wird.
Auf den ersten Blick mag es unintuitiv erscheinen, dass dieser Code weiterhin funktioniert. In einigen Programmiersprachen existieren die lokalen Variablen innerhalb einer Funktion nur für die Dauer der Ausführung dieser Funktion. SobaldmakeFunc() die Ausführung abgeschlossen hat, könnte man erwarten, dass diename-Variable nicht mehr zugänglich ist. Da der Code jedoch weiterhin funktioniert, ist dies offensichtlich nicht der Fall in JavaScript.
Der Grund dafür ist, dass Funktionen in JavaScript Closures bilden. EinClosure ist die Kombination einer Funktion und der lexikalischen Umgebung, in der diese Funktion deklariert wurde. Diese Umgebung besteht aus allen Variablen, die zum Zeitpunkt der Erstellung des Closures im Scope waren. In diesem Fall istmyFunc eine Referenz auf die Instanz der FunktiondisplayName, die erstellt wird, wennmakeFunc ausgeführt wird. Die Instanz vondisplayName behält eine Referenz auf ihre lexikalische Umgebung, in der die Variablename existiert. Aus diesem Grund bleibt die Variablename verfügbar, wennmyFunc aufgerufen wird, und "Mozilla" wird anconsole.log übergeben.
Hier ist ein etwas interessanteres Beispiel — einemakeAdder-Funktion:
function makeAdder(x) { return function (y) { return x + y; };}const add5 = makeAdder(5);const add10 = makeAdder(10);console.log(add5(2)); // 7console.log(add10(2)); // 12In diesem Beispiel haben wir eine FunktionmakeAdder(x) definiert, die ein einziges Argumentx annimmt und eine neue Funktion zurückgibt. Die Funktion, die sie zurückgibt, nimmt ein einziges Argumenty an und gibt die Summe vonx undy zurück.
Im Wesentlichen istmakeAdder eine Funktionsfabrik. Sie erstellt Funktionen, die einen bestimmten Wert zu ihrem Argument hinzufügen können. In dem obigen Beispiel erstellt die Funktionsfabrik zwei neue Funktionen — eine, die fünf zu ihrem Argument addiert, und eine, die 10 addiert.
add5 undadd10 bilden beide Closures. Sie teilen sich die gleiche Funktionskörperdefinition, speichern jedoch unterschiedliche lexikalische Umgebungen. Inadd5' lexikalischer Umgebung istx 5, während in der lexikalischen Umgebung füradd10x 10 ist.
Praktische Closures
Closures sind nützlich, weil sie es Ihnen ermöglichen, Daten (die lexikalische Umgebung) mit einer Funktion zu verknüpfen, die diese Daten verarbeitet. Dies hat offensichtliche Parallelen zur objektorientierten Programmierung, bei der Objekte es Ihnen ermöglichen, Daten (die Eigenschaften des Objekts) mit einer oder mehreren Methoden zu verknüpfen.
Folglich können Sie ein Closure überall dort verwenden, wo Sie normalerweise ein Objekt mit nur einer Methode verwenden würden.
Situationen, in denen Sie dies tun möchten, sind insbesondere im Web häufig. Ein Großteil des in Frontend-JavaScript geschriebenen Codes ist ereignisbasiert. Sie definieren nach einem bestimmten Verhalten und verknüpfen es dann mit einem Ereignis, das vom Benutzer ausgelöst wird (wie ein Klick oder ein Tastendruck). Der Code wird als Rückruf (eine einzelne Funktion, die als Reaktion auf das Ereignis ausgeführt wird) angehängt.
Angenommen, wir möchten der Seite Tasten hinzufügen, um die Textgröße anzupassen. Eine Möglichkeit, dies zu tun, besteht darin, die Schriftgröße des<body>-Elements (in Pixeln) festzulegen und dann die Größe der anderen Elemente auf der Seite (z. B. Header) mit der relativenem-Einheit zu setzen:
body { font-family: "Helvetica", "Arial", sans-serif; font-size: 12px;}h1 { font-size: 1.5em;}h2 { font-size: 1.2em;}Solche interaktiven Textgrößenknöpfe können diefont-size-Eigenschaft des<body>-Elements ändern, und die Anpassungen werden dank der relativen Einheiten von anderen Elementen auf der Seite übernommen.
Hier ist das #"size-12").onclick = size12;document.getElementById("size-14").onclick = size14;document.getElementById("size-16").onclick = size16;
<button>12</button><button>14</button><button>16</button><p>This is some text that will change size when you click the buttons above.</p>Private Methoden mit Closures emulieren
Sprachen wie Java erlauben es Ihnen, Methoden als privat zu deklarieren, was bedeutet, dass sie nur von anderen Methoden in derselben Klasse aufgerufen werden können.
JavaScript hatte vor der Einführung vonKlassen keine native Möglichkeit,private Methoden zu deklarieren, aber es war möglich, private Methoden mit Closures zu emulieren. Private Methoden sind nicht nur nützlich, um den Zugriff auf Code zu beschränken. Sie bieten auch eine leistungsstarke Möglichkeit, Ihren globalen Namespace zu verwalten.
Der folgende Code zeigt, wie man Closures verwendet, um öffentliche Funktionen zu definieren, die auf private Funktionen und Variablen zugreifen können. Beachten Sie, dass diese Closures demModule Design Pattern folgen.
const counter = (function () { let privateCounter = 0; function changeBy(val) { privateCounter += val; } return { increment() { changeBy(1); }, decrement() { changeBy(-1); }, value() { return privateCounter; }, };})();console.log(counter.value()); // 0.counter.increment();counter.increment();console.log(counter.value()); // 2.counter.decrement();console.log(counter.value()); // 1.In früheren Beispielen hatte jedes Closure seine eigene lexikalische Umgebung. Hier jedoch gibt es eine einzige lexikalische Umgebung, die von den drei Funktionen geteilt wird:counter.increment,counter.decrement undcounter.value.
Die geteilte lexikalische Umgebung wird im Rumpf einer anonymen Funktion erstellt,die ausgeführt wird, sobald sie definiert ist (auch bekannt alsIIFE). Die lexikalische Umgebung enthält zwei private Elemente: eine Variable namensprivateCounter und eine Funktion namenschangeBy. Sie können auf keines dieser privaten Mitglieder von außerhalb der anonymen Funktion zugreifen. Stattdessen greifen Sie indirekt über die drei öffentlichen Funktionen, die aus dem anonymen Wrapper zurückgegeben werden, darauf zu.
Diese drei öffentlichen Funktionen bilden Closures, die die gleiche lexikalische Umgebung teilen. Dank der lexikalischen Scoping von JavaScript haben sie alle Zugriff auf die VariableprivateCounter und die FunktionchangeBy.
function makeCounter() { let privateCounter = 0; function changeBy(val) { privateCounter += val; } return { increment() { changeBy(1); }, decrement() { changeBy(-1); }, value() { return privateCounter; }, };}const counter1 = makeCounter();const counter2 = makeCounter();console.log(counter1.value()); // 0.counter1.increment();counter1.increment();console.log(counter1.value()); // 2.counter1.decrement();console.log(counter1.value()); // 1.console.log(counter2.value()); // 0.Beachten Sie, wie die beiden Counter ihre Unabhängigkeit voneinander bewahren. Jedes Closure bezieht sich auf eine andere Version derprivateCounter-Variable durch sein eigenes Closure. Jedes Mal, wenn einer der Counter aufgerufen wird, ändert sich seine lexikalische Umgebung, indem der Wert dieser Variable geändert wird. Änderungen am Variablenwert in einem Closure beeinflussen nicht den Wert im anderen Closure.
Hinweis:Die Verwendung von Closures auf diese Weise bietet Vorteile, die normalerweise mit objektorientierter Programmierung verbunden sind. InsbesondereDatenverbergen undKapselung.
Closure-Scope-Kette
Der Zugriff einer verschachtelten Funktion auf den Scope der äußeren Funktion schließt den umgebenden Scope der äußeren Funktion ein und erstellt effektiv eine Kette von Funktionsscopes. Um dies zu demonstrieren, betrachten Sie den folgenden Beispielcode.
// global scopeconst e = 10;function sum(a) { return function (b) { return function (c) { // outer functions scope return function (d) { // local scope return a + b + c + d + e; }; }; };}console.log(sum(1)(2)(3)(4)); // 20Sie können auch ohne anonyme Funktionen schreiben:
// global scopeconst e = 10;function sum(a) { return function sum2(b) { return function sum3(c) { // outer functions scope return function sum4(d) { // local scope return a + b + c + d + e; }; }; };}const sum2 = sum(1);const sum3 = sum2(2);const sum4 = sum3(3);const result = sum4(4);console.log(result); // 20Im obigen Beispiel gibt es eine Reihe von verschachtelten Funktionen, die alle Zugriff auf den Scope der äußeren Funktionen haben. In diesem Kontext können wir sagen, dass Closures Zugriff aufalle äußeren Scopes haben.
Closures können auch Variablen in Blockscopes und Moduls scopes erfassen. Zum Beispiel wird im Folgenden ein Closure über die block-scoped Variabley erstellt:
function outer() { let getY; { const y = 6; getY = () => y; } console.log(typeof y); // undefined console.log(getY()); // 6}outer();Closures über Module können interessanter sein.
// myModule.jslet x = 5;export const getX = () => x;export const setX = (val) => { x = val;};Hier exportiert das Modul ein Paar von Getter-Setter-Funktionen, die über die im Modul gescoped Variablex schließen. Auch wennx nicht direkt von anderen Modulen zugänglich ist, kann es mit den Funktionen gelesen und geschrieben werden.
import { getX, setX } from "./myModule.js";console.log(getX()); // 5setX(6);console.log(getX()); // 6Closures können auch über importierte Werte schließen, die alsdynamischeBindings betrachtet werden, weil sich der importierte Wert entsprechend ändert, wenn sich der Originalwert ändert.
// myModule.jsexport let x = 1;export const setX = (val) => { x = val;};// closureCreator.jsimport { x } from "./myModule.js";export const getX = () => x; // Close over an imported live bindingimport { getX } from "./closureCreator.js";import { setX } from "./myModule.js";console.log(getX()); // 1setX(2);console.log(getX()); // 2Erstellen von Closures in Schleifen: Ein häufiger Fehler
Vor der Einführung deslet Schlüsselworts trat ein häufiges Problem mit Closures auf, wenn Sie sie innerhalb einer Schleife erstellten. Um dies zu demonstrieren, betrachten Sie den folgenden Beispielcode.
<p>Helpful notes will appear here</p><p>Email: <input type="text" name="email" /></p><p>Name: <input type="text" name="name" /></p><p>Age: <input type="text" name="age" /></p>function showHelp(help) { document.getElementById("help").textContent = help;}function setupHelp() { var helpText = [ { id: "email", help: "Your email address" }, { id: "name", help: "Your full name" }, { id: "age", help: "Your age (you must be over 16)" }, ]; for (var i = 0; i < helpText.length; i++) { // Culprit is the use of `var` on this line var item = helpText[i]; document.getElementById(item.id).onfocus = function () { showHelp(item.help); }; }}setupHelp();DashelpText-Array definiert drei hilfreiche Hinweise, die jeweils mit der ID eines Eingabefeldes im Dokument verknüpft sind. Die Schleife durchläuft diese Definitionen und verbindet einonfocus-Ereignis mit jedem, das die zugehörige Hilfsmethode anzeigt.
Wenn Sie diesen Code ausprobieren, werden Sie sehen, dass er nicht wie erwartet funktioniert. Unabhängig davon, auf welches Feld Sie sich konzentrieren, wird die Nachricht über Ihr Alter angezeigt.
Der Grund dafür ist, dass die Funktionen, dieonfocus zugewiesen wurden, Closures bilden; sie bestehen aus der Funktionsdefinition und der erfassten Umgebung aus dem Scope dersetupHelp-Funktion. Drei Closures wurden durch die Schleife erstellt, aber jedes teilt die gleiche einzelne lexikalische Umgebung, die eine Variable mit sich ändernden Werten (item) hat. Dies liegt daran, dass die Variableitem mitvar deklariert ist und daher aufgrund des Hoistings Funktions-Scope hat. Der Wert vonitem.help wird bestimmt, wenn dieonfocus-Rückrufe ausgeführt werden. Da die Schleife zu diesem Zeitpunkt bereits durchgelaufen ist, zeigt dasitem-Variablenobjekt (das von allen drei Closures geteilt wird) auf den letzten Eintrag in derhelpText-Liste.
Eine Lösung in diesem Fall besteht darin, mehr Closures zu verwenden: Insbesondere eine Funktionsfabrik, wie zuvor beschrieben:
<p>Helpful notes will appear here</p><p>Email: <input type="text" name="email" /></p><p>Name: <input type="text" name="name" /></p><p>Age: <input type="text" name="age" /></p>function showHelp(help) { document.getElementById("help").textContent = help;}function makeHelpCallback(help) { return function () { showHelp(help); };}function setupHelp() { var helpText = [ { id: "email", help: "Your email address" }, { id: "name", help: "Your full name" }, { id: "age", help: "Your age (you must be over 16)" }, ]; for (var i = 0; i < helpText.length; i++) { var item = helpText[i]; document.getElementById(item.id).onfocus = makeHelpCallback(item.help); }}setupHelp();Dies funktioniert wie erwartet. Anstatt dass alle Rückrufe eine einzelne lexikalische Umgebung teilen, erstellt diemakeHelpCallback-Funktioneine neue lexikalische Umgebung für jeden Rückruf, in derhelp sich auf den entsprechenden String aus demhelpText-Array bezieht.
Eine andere Möglichkeit, das obige mit anonymen Closures zu schreiben, ist:
function showHelp(help) { document.getElementById("help").textContent = help;}function setupHelp() { var helpText = [ { id: "email", help: "Your email address" }, { id: "name", help: "Your full name" }, { id: "age", help: "Your age (you must be over 16)" }, ]; for (var i = 0; i < helpText.length; i++) { (function () { var item = helpText[i]; document.getElementById(item.id).onfocus = function () { showHelp(item.help); }; })(); // Immediate event listener attachment with the current value of item (preserved until iteration). }}setupHelp();Wenn Sie nicht mehr Closures verwenden möchten, können Sie daslet- oderconst-Schlüsselwort verwenden:
function showHelp(help) { document.getElementById("help").textContent = help;}function setupHelp() { const helpText = [ { id: "email", help: "Your email address" }, { id: "name", help: "Your full name" }, { id: "age", help: "Your age (you must be over 16)" }, ]; for (let i = 0; i < helpText.length; i++) { const item = helpText[i]; document.getElementById(item.id).onfocus = () => { showHelp(item.help); }; }}setupHelp();In diesem Beispiel wirdconst anstelle vonvar verwendet, sodass jedes Closure die block-gescopte Variable bindet, was bedeutet, dass keine zusätzlichen Closures erforderlich sind.
Wenn Sie ohnehin modernen JavaScript schreiben, können Sie weitere Alternativen zur einfachenfor-Schleife in Betracht ziehen, wie z. B. die Verwendung vonfor...of-Schleifen unditem alslet oderconst zu deklarieren oder dieforEach()-Methode zu verwenden, die beide das Closure-Problem vermeiden.
for (const item of helpText) { document.getElementById(item.id).onfocus = () => { document.getElementById("help").textContent = item.help; };}helpText.forEach((item) => { document.getElementById(item.id).onfocus = () => { showHelp(item.help); };});Leistungsüberlegungen
Wie zuvor erwähnt, verwaltet jede Funktionsinstanz ihren eigenen Scope und ihr eigenes Closure. Daher ist es unklug, unnötig Funktionen innerhalb anderer Funktionen zu erstellen, wenn Closures für eine bestimmte Aufgabe nicht benötigt werden, da dies negative Auswirkungen auf die Skriptleistung sowohl in Bezug auf Verarbeitungsgeschwindigkeit als auch Speicherverbrauch haben wird.
Wenn Sie beispielsweise ein neues Objekt/eine neue Klasse erstellen, sollten Methoden normalerweise mit dem Prototyp des Objekts verknüpft werden, anstatt in den Objektkonstruktor definiert zu werden. Der Grund dafür ist, dass die Methoden jedes Mal, wenn der Konstruktor aufgerufen wird, neu zugewiesen würden (d.h. für jede Objekterstellung).
Betrachten Sie den folgenden Fall:
function MyObject(name, message) { this.name = name.toString(); this.message = message.toString(); this.getName = function () { return this.name; }; this.getMessage = function () { return this.message; };}Da der vorherige Code in diesem speziellen Fall nicht die Vorteile der Verwendung von Closures nutzt, könnten wir ihn stattdessen umschreiben, um die Verwendung von Closures zu vermeiden:
function MyObject(name, message) { this.name = name.toString(); this.message = message.toString();}MyObject.prototype = { getName() { return this.name; }, getMessage() { return this.message; },};Die Neudefinition des Prototyps wird jedoch nicht empfohlen. Im folgenden Beispiel wird dem vorhandenen Prototyp stattdessen etwas hinzugefügt:
function MyObject(name, message) { this.name = name.toString(); this.message = message.toString();}MyObject.prototype.getName = function () { return this.name;};MyObject.prototype.getMessage = function () { return this.message;};In den beiden vorherigen Beispielen kann der geerbte Prototyp von allen Objekten gemeinsam genutzt werden und die Methodendefinitionen müssen nicht bei jeder Objekterstellung erfolgen. Weitere Informationen finden Sie unterVererbung und die Prototypkette.