Schließungen sind eine etwas fortgeschrittene und oft missverstandene Funktion der JavaScript-Sprache. Einfach ausgedrückt, sind Closures Objekte, die eine Funktion und einen Verweis auf die Umgebung enthalten, in der die Funktion erstellt wurde. Um Closures vollständig zu verstehen, muss man jedoch zunächst zwei andere Merkmale der JavaScript-Sprache verstehen: Funktionen erster Klasse und innere Funktionen.
Erstklassige Funktionen
In Programmiersprachen werden Funktionen als Bürger erster Klasse betrachtet, wenn sie wie jeder andere Datentyp manipuliert werden können. Erstklassige Funktionen können zum Beispiel zur Laufzeit konstruiert und Variablen zugewiesen werden. Sie können auch an andere Funktionen weitergegeben und von diesen zurückgegeben werden. JavaScript-Funktionen erfüllen nicht nur die oben genannten Kriterien, sondern haben auch ihre eigenen Eigenschaften und Methoden. Das folgende Beispiel zeigt einige der Möglichkeiten von Funktionen erster Klasse. In diesem Beispiel werden zwei Funktionen erstellt und den Variablen "foo" und "bar" zugewiesen. Die in "foo" gespeicherte Funktion zeigt ein Dialogfeld an, während "bar" einfach das Argument zurückgibt, das ihr übergeben wurde. Die letzte Zeile des Beispiels bewirkt mehrere Dinge. Zunächst wird die in "bar" gespeicherte Funktion mit "foo" als Argument aufgerufen. Anschließend gibt "bar" den Verweis auf die Funktion "foo" zurück. Schließlich wird die zurückgegebene "foo"-Referenz aufgerufen, wodurch "Hello World!" angezeigt wird.
var foo = function() {
alert("Hello World!");
};
var bar = function(arg) {
return arg;
};
bar(foo)();
Innere Funktionen
Innere Funktionen, die auch als verschachtelte Funktionen bezeichnet werden, sind Funktionen, die innerhalb einer anderen Funktion (der äußeren Funktion) definiert sind. Jedes Mal, wenn die äußere Funktion aufgerufen wird, wird eine Instanz der inneren Funktion erstellt. Das folgende Beispiel zeigt, wie innere Funktionen verwendet werden. In diesem Fall ist add() die äußere Funktion. Innerhalb von add() wird die innere Funktion doAdd() definiert und aufgerufen.
function add(value1, value2) {
function doAdd(operand1, operand2) {
return operand1 + operand2;
}
return doAdd(value1, value2);
}
var foo = add(1, 2);
// foo equals 3
Ein wichtiges Merkmal innerer Funktionen ist, dass sie impliziten Zugriff auf den Geltungsbereich der äußeren Funktion haben. Das bedeutet, dass die innere Funktion die Variablen, Argumente usw. der äußeren Funktion verwenden kann. Im vorigen Beispiel hat die " Wert1 " und " Wert2 " Argumente von add() wurden an doAdd() als die " Operand1 " und "operand2" Argumente. Dies ist jedoch unnötig, da doAdd() hat direkten Zugang zu " Wert1 " und " Wert2 ". Das vorherige Beispiel wurde im Folgenden umgeschrieben, um zu zeigen, wie doAdd() verwenden können " Wert1 " und " Wert2 ".
function add(value1, value2) {
function doAdd() {
return value1 + value2;
}
return doAdd();
}
var foo = add(1, 2);
// foo equals 3
Verschlüsse erstellen
Ein Abschluss wird erstellt, wenn eine innere Funktion von außerhalb der Funktion, die sie erstellt hat, zugänglich gemacht wird. Dies geschieht typischerweise, wenn eine äußere Funktion eine innere Funktion zurückgibt. Wenn dies geschieht, erhält die innere Funktion einen Verweis auf die Umgebung, in der sie erstellt wurde erstellt wurde. Das bedeutet, dass sie sich an alle Variablen (und ihre Werte), die zu diesem Zeitpunkt im Gültigkeitsbereich waren. Das folgende Beispiel zeigt, wie eine Closure erstellt und verwendet wird.
function add(value1) {
return function doAdd(value2) {
return value1 + value2;
};
}
var increment = add(1);
var foo = increment(2);
// foo equals 3
Bei diesem Beispiel gibt es eine Reihe von Besonderheiten zu beachten.
Die Funktion add() gibt ihre innere Funktion doAdd() zurück. Durch die Rückgabe eines Verweises auf eine innere Funktion wird ein Abschluss erstellt. "value1" ist eine lokale Variable von add() und eine nicht-lokale Variable von doAdd(). Nichtlokale Variablen beziehen sich auf Variablen, die sich weder im lokalen noch im globalen Bereich befinden. "value2" ist eine lokale Variable von doAdd(). Wenn add(1) aufgerufen wird, wird eine Schließung erstellt und in "increment" gespeichert. In der referenzierenden Umgebung der Closure wird "value1" an den Wert eins gebunden. Variablen, die gebunden sind, werden auch als Closure bezeichnet. Daher kommt auch der Name Closure. Wenn increment(2) aufgerufen wird, wird die Closure eingegeben. Das bedeutet, dass doAdd() aufgerufen wird, wobei die Variable "value1" den Wert eins hat. Die Schließung kann man sich im Wesentlichen so vorstellen, dass sie die folgende Funktion erzeugt.
function increment(value2) {
return 1 + value2;
}
Wann sind Verschlüsse zu verwenden?
Mit Verschlüssen lassen sich viele Dinge erreichen. Sie sind sehr nützlich für Dinge wie die Konfiguration von Callback-Funktionen mit Parametern. Dieser Abschnitt behandelt zwei Szenarien, in denen Closures Ihr Leben als Entwickler viel einfacher machen können.
Arbeiten mit Timern
Closures sind nützlich, wenn sie in Verbindung mit dem setTimeout() y setInterval() Funktionen. Um genauer zu sein, ermöglichen Closures die Übergabe von Argumenten an die Callback-Funktionen von setTimeout() y setInterval() . Der folgende Code druckt zum Beispiel die Zeichenkette "some message" einmal pro Sekunde, indem er Folgendes aufruft showMessage() .
<!DOCTYPE html>
<html lang="en">
<head>
<title>Closures</title>
<meta charset="UTF-8" />
<script>
window.addEventListener("load", function() {
window.setInterval(showMessage, 1000, "some message<br />");
});
function showMessage(message) {
document.getElementById("message").innerHTML += message;
}
</script>
</head>
<body>
<span id="message"></span>
</body>
</html>
Leider unterstützt der Internet Explorer die Übergabe von Callback-Argumenten über setInterval() nicht. Anstatt "some message" anzuzeigen, zeigt Internet Explorer "undefined" an (da kein Wert an showMessage() übergeben wird). Um dieses Problem zu umgehen, kann eine Closure erstellt werden, die das Argument "message" an den gewünschten Wert bindet. Die Closure kann dann als Callback-Funktion für setInterval() verwendet werden. Zur Veranschaulichung dieses Konzepts wurde der JavaScript-Code aus dem vorherigen Beispiel umgeschrieben, um eine Closure zu verwenden.
window.addEventListener("load", function() {
var showMessage = getClosure("some message<br />");
window.setInterval(showMessage, 1000);
});
function getClosure(message) {
function showMessage() {
document.getElementById("message").innerHTML += message;
}
return showMessage;
}
Emulation privater Daten
Viele objektorientierte Sprachen unterstützen das Konzept der privaten Mitgliedsdaten. JavaScript ist jedoch keine rein objektorientierte Sprache und unterstützt keine privaten Daten. Es ist jedoch möglich, private Daten mit Hilfe von Closures zu emulieren. Eine Closure enthält einen Verweis auf die Umgebung, in der sie ursprünglich erstellt wurde und die nun außerhalb des Anwendungsbereichs liegt. Da die Variablen in der referenzierenden Umgebung nur von der Closure-Funktion aus zugänglich sind, handelt es sich im Wesentlichen um private Daten.
Das folgende Beispiel zeigt einen Konstruktor für eine einfache Personenklasse. Wenn jede Person erstellt wird, erhält sie einen Namen über das " Name Argument". Intern speichert die Person ihren Namen in der Datei " _Name Variable". Nach guter objektorientierter Programmierpraxis wird die Methode getName() ist ebenfalls vorgesehen, um den Namen abzurufen.
function Person(name) {
this._name = name;
this.getName = function() {
return this._name;
};
}
Es gibt noch ein großes Problem mit der Klasse Person. Da JavaScript keine privaten Daten unterstützt, gibt es nichts, was jemanden daran hindert, den Namen zu ändern. Der folgende Code erstellt beispielsweise eine Person namens Colin und ändert dann ihren Namen in Tom.
var person = new Person("Colin");
person._name = "Tom";
// person.getName() now returns "Tom"
Ich persönlich würde es nicht gut finden, wenn einfach jeder kommen und meinen Namen legal ändern könnte. Um dies zu verhindern, kann die Variable "_name" durch einen Closure privat gemacht werden. Der Konstruktor von Person wurde im Folgenden unter Verwendung einer Closure umgeschrieben. Beachten Sie, dass "_name" nun eine lokale Variable des Person-Konstruktors ist und nicht mehr eine Objekteigenschaft. Eine Closure wird gebildet, weil die äußere Funktion, Person() legt eine innere Funktion offen, indem es die öffentliche getName() Methode.
function Person(name) {
var _name = name;
this.getName = function() {
return _name;
};
}
Wenn nun getName() aufgerufen wird, wird garantiert der Wert zurückgegeben, der ursprünglich an den Konstruktor übergeben wurde. Es ist immer noch möglich, dass jemand dem Objekt eine neue Eigenschaft "_name" hinzufügt, aber die interne Funktionsweise des Objekts wird nicht beeinträchtigt, solange sie sich auf die durch die Schließung gebundene Variable bezieht. Der folgende Code zeigt, dass die Variable "_name" in der Tat privat ist.
var person = new Person("Colin");
person._name = "Tom";
// person._name is "Tom" but person.getName() returns "Colin"
Wann keine Verschlüsse verwendet werden sollten
Es ist wichtig zu verstehen, wie Verschlüsse funktionieren und wann sie verwendet werden sollten. Ebenso wichtig ist es, zu wissen, wann sie nicht das richtige Werkzeug für die anstehende Aufgabe sind. Die übermäßige Verwendung von Closures kann dazu führen, dass Skripte langsam ausgeführt langsam ausführen und unnötig viel Speicher verbrauchen. Und weil Closures so einfach einfach zu erstellen sind, ist es möglich, sie zu missbrauchen, ohne es zu wissen. es. Dieser Abschnitt behandelt mehrere Szenarien, in denen Closures mit Vorsicht verwendet werden sollten.
In Schleifen
Das Erstellen von Abschlüssen innerhalb von Schleifen kann zu irreführenden Ergebnissen führen. Ein Beispiel hierfür ist unten dargestellt. In diesem Beispiel werden drei Schaltflächen erstellt. Wenn "Schaltfläche1" angeklickt wird, sollte eine Meldung angezeigt werden, die besagt "Schaltfläche 1 angeklickt". Ähnliche Meldungen sollten auch für "Schaltfläche2" und "Schaltfläche3" angezeigt werden. Wenn dieser Code jedoch ausgeführt wird, zeigen alle Schaltflächen "Schaltfläche 4 angeklickt" an. Das liegt daran, dass die Schleife zu dem Zeitpunkt, an dem eine der Schaltflächen angeklickt wird, bereits abgeschlossen ist und die Schleifenvariable den Endwert vier erreicht hat.
<!DOCTYPE html>
<html lang="en">
<head>
<title>Closures</title>
<meta charset="UTF-8" />
<script>
window.addEventListener("load", function() {
for (var i = 1; i < 4; i++) {
var button = document.getElementById("button" + i);
button.addEventListener("click", function() {
alert("Clicked button " + i);
});
}
});
</script>
</head>
<body>
<input type="button" id="button1" value="One" />
<input type="button" id="button2" value="Two" />
<input type="button" id="button3" value="Three" />
</body>
</html>
Um dieses Problem zu lösen, muss die Closure von der eigentlichen Schleifenvariable entkoppelt werden. Dies kann durch den Aufruf einer neuen Funktion geschehen, die wiederum eine neue referenzierende Umgebung erzeugt. Das folgende Beispiel zeigt, wie dies gemacht wird. Die Schleifenvariable wird an die Funktion getHandler() übergeben. getHandler() gibt dann eine Schließung zurück, die von der ursprünglichen "for"-Schleife unabhängig ist.
function getHandler(i) {
return function handler() {
alert("Clicked button " + i);
};
}
window.addEventListener("load", function() {
for (var i = 1; i < 4; i++) {
var button = document.getElementById("button" + i);
button.addEventListener("click", getHandler(i));
}
});
Unnötige Verwendung in Konstrukteuren
Konstruktorfunktionen sind eine weitere häufige Quelle des Missbrauchs von Closures. Wir haben gesehen, wie Closures verwendet werden können, um private Daten zu emulieren. Wie auch immer, ist es jedoch übertrieben, Methoden als Closures zu implementieren, wenn sie nicht tatsächlich auf die privaten Daten zugreifen. Das folgende Beispiel greift auf die Klasse Person Klasse, fügt aber diesmal eine sayHello()-Methode hinzu, die nicht auf die privaten Daten verwendet.
function Person(name) {
var _name = name;
this.getName = function() {
return _name;
};
this.sayHello = function() {
alert("Hello!");
};
}
Jedes Mal, wenn eine Person instanziiert wird, wird Zeit damit verbracht, die sayHello() Methode. Wenn viele Person-Objekte erstellt werden, wird dies zu einer Zeitverschwendung. Ein besserer Ansatz wäre es, sayHello() in den Person Prototyp hinzuzufügen. Durch das Hinzufügen zum Prototyp können alle Person-Objekte dieselbe Methode verwenden. Dies spart Zeit im Konstruktor, da man nicht nicht für jede Instanz eine Closure erstellen muss. Das vorherige Beispiel wird umgeschrieben, wobei die überflüssige Closure in den Prototyp verschoben wurde.
function Person(name) {
var _name = name;
this.getName = function() {
return _name;
};
}
Person.prototype.sayHello = function() {
alert("Hello!");
};
Dinge zum Merken
- Closures enthalten eine Funktion und einen Verweis auf die Umgebung in in der die Funktion erstellt wurde.
- Ein Abschluss wird gebildet, wenn eine äußere Funktion eine innere Funktion freilegt. Closures können verwendet werden, um auf einfache Weise Parameter an Callback-Funktionen zu übergeben.
- Private Daten können durch die Verwendung von Schließungen emuliert werden. Dies ist üblich in objektorientierten Programmierung und Namespace-Design üblich.
- Closures sollten nicht übermäßig in Konstruktoren verwendet werden. Das Hinzufügen zum Prototyp ist eine bessere Idee.
Link