7619 Stimmen

Wie funktionieren die JavaScript-Schließungen?

Wie würden Sie JavaScript-Schließungen jemandem erklären, der zwar die Konzepte kennt, aus denen sie bestehen (z. B. Funktionen, Variablen und Ähnliches), aber die Schließungen selbst nicht versteht?

Ich habe gesehen das Beispiel Schema auf Wikipedia angegeben, aber leider hat es nicht geholfen.

8331voto

Joel Anair Punkte 13536

Ein Verschluss ist eine Paarung von:

  1. Eine Funktion und
  2. Ein Verweis auf den äußeren Bereich dieser Funktion (lexikalische Umgebung)

Eine lexikalische Umgebung ist Teil jedes Ausführungskontextes (Stackframe) und ist eine Abbildung zwischen Bezeichnern (d.h. lokalen Variablennamen) und Werten.

Jede Funktion in JavaScript unterhält einen Verweis auf ihre äußere lexikalische Umgebung. Dieser Verweis wird verwendet, um den Ausführungskontext zu konfigurieren, der beim Aufrufen einer Funktion erstellt wird. Dieser Verweis ermöglicht es dem Code innerhalb der Funktion, außerhalb der Funktion deklarierte Variablen zu "sehen", unabhängig davon, wann und wo die Funktion aufgerufen wird.

Wurde eine Funktion von einer Funktion aufgerufen, die wiederum von einer anderen Funktion aufgerufen wurde, so entsteht eine Kette von Verweisen auf äußere lexikalische Umgebungen. Diese Kette wird als Scope-Kette bezeichnet.

Im folgenden Code, inner bildet einen Abschluss mit der lexikalischen Umgebung des Ausführungskontextes, der entsteht, wenn foo aufgerufen wird, zu Ende gehend variabel secret :

function foo() {
  const secret = Math.trunc(Math.random() * 100)
  return function inner() {
    console.log(`The secret number is ${secret}.`)
  }
}
const f = foo() // `secret` is not directly accessible from outside `foo`
f() // The only way to retrieve `secret`, is to invoke `f`

Mit anderen Worten: In JavaScript enthalten Funktionen einen Verweis auf eine private "Box of State", auf die nur sie (und alle anderen in derselben lexikalischen Umgebung deklarierten Funktionen) Zugriff haben. Diese Zustandsbox ist für den Aufrufer der Funktion unsichtbar und bietet einen hervorragenden Mechanismus zum Verstecken und zur Kapselung von Daten.

Und denken Sie daran: Funktionen in JavaScript können wie Variablen weitergegeben werden (Funktionen erster Klasse), d. h. diese Paare von Funktionen und Zuständen können in Ihrem Programm weitergegeben werden: ähnlich wie Sie eine Instanz einer Klasse in C++ weitergeben könnten.

Gäbe es in JavaScript keine Abschlüsse, müssten mehr Zustände zwischen Funktionen übergeben werden ausdrücklich Dadurch werden die Parameterlisten länger und der Code verrauscht.

Wenn Sie also wollen, dass eine Funktion immer Zugriff auf einen privaten Teil des Zustands hat, können Sie einen Abschluss verwenden.

...und häufig sind wir faire den Zustand mit einer Funktion verknüpfen wollen. Wenn Sie zum Beispiel in Java oder C++ einer Klasse eine private Instanzvariable und eine Methode hinzufügen, verknüpfen Sie den Zustand mit einer Funktion.

In C und den meisten anderen gängigen Sprachen ist nach der Rückkehr einer Funktion kein Zugriff mehr auf alle lokalen Variablen möglich, da der Stack-Frame zerstört wird. Wenn Sie in JavaScript eine Funktion innerhalb einer anderen Funktion deklarieren, können die lokalen Variablen der äußeren Funktion auch nach der Rückkehr von dieser zugänglich bleiben. Auf diese Weise wird im obigen Code, secret bleibt für das Funktionsobjekt verfügbar inner , es wurde zurückgegeben von foo .

Verwendungszwecke von Verschlüssen

Closures sind immer dann nützlich, wenn Sie einen privaten Zustand in Verbindung mit einer Funktion benötigen. Dies ist ein sehr häufiges Szenario - und denken Sie daran: JavaScript verfügte bis 2015 nicht über eine Klassensyntax und hat immer noch keine Syntax für private Felder. Closures erfüllen diesen Bedarf.

Private Instanzvariablen

In dem folgenden Code wird die Funktion toString schließt die Details des Fahrzeugs.

function Car(manufacturer, model, year, color) {
  return {
    toString() {
      return `${manufacturer} ${model} (${year}, ${color})`
    }
  }
}

const car = new Car('Aston Martin', 'V8 Vantage', '2012', 'Quantum Silver')
console.log(car.toString())

Funktionale Programmierung

In dem folgenden Code wird die Funktion inner schließt sich über beide fn y args .

function curry(fn) {
  const args = []
  return function inner(arg) {
    if(args.length === fn.length) return fn(...args)
    args.push(arg)
    return inner
  }
}

function add(a, b) {
  return a + b
}

const curriedAdd = curry(add)
console.log(curriedAdd(2)(3)()) // 5

Ereignisorientiertes Programmieren

Im folgenden Code wird die Funktion onClick schließt über Variable BACKGROUND_COLOR .

const $ = document.querySelector.bind(document)
const BACKGROUND_COLOR = 'rgba(200, 200, 242, 1)'

function onClick() {
  $('body').style.background = BACKGROUND_COLOR
}

$('button').addEventListener('click', onClick)

<button>Set background color</button>

Modularisierung

Im folgenden Beispiel sind alle Implementierungsdetails in einem unmittelbar ausgeführten Funktionsausdruck verborgen. Die Funktionen tick y toString den privaten Zustand und die Funktionen zu schließen, die sie für ihre Arbeit benötigen. Closures haben es uns ermöglicht, unseren Code zu modularisieren und zu kapseln.

let namespace = {};

(function foo(n) {
  let numbers = []

  function format(n) {
    return Math.trunc(n)
  }

  function tick() {
    numbers.push(Math.random() * 100)
  }

  function toString() {
    return numbers.map(format)
  }

  n.counter = {
    tick,
    toString
  }
}(namespace))

const counter = namespace.counter
counter.tick()
counter.tick()
console.log(counter.toString())

Beispiele

Beispiel 1

Dieses Beispiel zeigt, dass die lokalen Variablen nicht in die Schließung kopiert werden: die Schließung behält einen Verweis auf die ursprünglichen Variablen bei selbst . Es ist, als ob der Stack-Frame auch nach Beendigung der äußeren Funktion im Speicher erhalten bleibt.

function foo() {
  let x = 42
  let inner = () => console.log(x)
  x = x + 1
  return inner
}

foo()() // logs 43

Beispiel 2

Der folgende Code enthält drei Methoden log , increment et update alle über dieselbe lexikalische Umgebung schließen.

Und jedes Mal createObject aufgerufen wird, wird ein neuer Ausführungskontext (Stack-Frame) erstellt und eine völlig neue Variable x und eine neue Reihe von Funktionen ( log usw.) erstellt, die über diese neue Variable schließen.

function createObject() {
  let x = 42;
  return {
    log() { console.log(x) },
    increment() { x++ },
    update(value) { x = value }
  }
}

const o = createObject()
o.increment()
o.log() // 43
o.update(5)
o.log() // 5
const p = createObject()
p.log() // 42

Beispiel 3

Wenn Sie Variablen verwenden, die mit var Achten Sie darauf, dass Sie wissen, welche Variable Sie schließen. Variablen, die mit var hochgezogen werden. In modernem JavaScript ist dieses Problem dank der Einführung von let y const .

Im folgenden Code wird jedes Mal, wenn die Schleife durchlaufen wird, eine neue Funktion inner erstellt, das sich über i . Aber weil var i aus der Schleife herausgezogen wird, schließen alle diese inneren Funktionen über dieselbe Variable, was bedeutet, dass der Endwert von i (3) wird dreimal gedruckt.

function foo() {
  var result = []
  for (var i = 0; i < 3; i++) {
    result.push(function inner() { console.log(i) } )
  }

  return result
}

const result = foo()
// The following will print `3`, three times...
for (var i = 0; i < 3; i++) {
  result[i]() 
}

Letzte Punkte:

  • Wann immer eine Funktion in JavaScript deklariert wird, wird ein Abschluss erstellt.
  • Rücksendung einer function innerhalb einer anderen Funktion ist das klassische Beispiel für einen Abschluss, da der Zustand innerhalb der äußeren Funktion implizit für die zurückgegebene innere Funktion verfügbar ist, selbst nachdem die äußere Funktion ihre Ausführung beendet hat.
  • Wann immer Sie eval() innerhalb einer Funktion wird ein Abschluss verwendet. Der Text, den Sie eval kann auf lokale Variablen der Funktion verweisen, und im nicht-strikten Modus können Sie sogar neue lokale Variablen erstellen, indem Sie eval('var foo = …') .
  • Wenn Sie new Function(…) (die Funktionskonstrukteur ) innerhalb einer Funktion, schließt sie sich nicht über ihre lexikalische Umgebung ab: Sie schließt sich stattdessen über den globalen Kontext ab. Die neue Funktion kann nicht auf die lokalen Variablen der äußeren Funktion verweisen.
  • Ein Abschluss in JavaScript ist wie eine Referenz ( NO eine Kopie) an den Bereich am Punkt der Funktionsdeklaration, der wiederum einen Verweis auf seinen äußeren Bereich behält, und so weiter, bis hin zum globalen Objekt am Anfang der Bereichskette.
  • Bei der Deklaration einer Funktion wird eine Closure erstellt, die beim Aufruf der Funktion zur Konfiguration des Ausführungskontextes verwendet wird.
  • Bei jedem Funktionsaufruf wird ein neuer Satz lokaler Variablen erstellt.

Links

11 Stimmen

Ich bin erst seit sechs Jahren Frontend-Entwickler, daher bin ich neugierig, wie gängige Beispiele wie curriedAdd(2)(3)() in Ihren Beispielen zur funktionalen Programmierung sind außerdem bei der Erklärung von Schließungen oder in Programmiergesprächen von Bedeutung. Ich habe viele Code-Reviews durchgeführt und bin noch nie darauf gestoßen, aber ich habe auch noch nie mit MVPs der Informatik gearbeitet, wie ich sie bei den FANG-Unternehmen vermute.

4144voto

Ali Punkte 954

Jede Funktion in JavaScript unterhält eine Verbindung zu ihrer äußeren lexikalischen Umgebung. Eine lexikalische Umgebung ist eine Karte aller Namen (z.B. Variablen, Parameter) innerhalb eines Bereichs, mit ihren Werten.

Wann immer Sie also die function Schlüsselwort, hat der Code innerhalb dieser Funktion Zugriff auf Variablen, die außerhalb der Funktion deklariert sind.

function foo(x) {
  var tmp = 3;

  function bar(y) {
    console.log(x + y + (++tmp)); // will log 16
  }

  bar(10);
}

foo(2);

Dies wird protokolliert 16 weil Funktion bar schließt sich über dem Parameter x und die Variable tmp , die beide in der lexikalischen Umgebung der äußeren Funktion existieren foo .

Funktion bar zusammen mit seiner Verbindung zum lexikalischen Umfeld der Funktion foo ist ein Abschluss.

Eine Funktion muss nicht return um einen Abschluss zu erstellen. Allein durch ihre Deklaration schließt jede Funktion die sie umgebende lexikalische Umgebung ab und bildet so eine Schließung.

function foo(x) {
  var tmp = 3;

  return function (y) {
    console.log(x + y + (++tmp)); // will also log 16
  }
}

var bar = foo(2);
bar(10); // 16
bar(10); // 17

Die obige Funktion wird auch 16 protokollieren, da der Code in bar kann sich immer noch auf das Argument x und variabel tmp auch wenn sie nicht mehr direkt betroffen sind.

Da jedoch tmp immer noch im Innern herumhängt bar ist es verfügbar, um erhöht zu werden. Sie wird jedes Mal erhöht, wenn Sie die bar .

Das einfachste Beispiel für einen Abschluss ist der folgende:

var a = 10;

function test() {
  console.log(a); // will output 10
  console.log(b); // will output 6
}
var b = 6;
test();

Wenn eine JavaScript-Funktion aufgerufen wird, wird ein neuer Ausführungskontext ec erstellt wird. Zusammen mit den Funktionsargumenten und dem Zielobjekt erhält dieser Ausführungskontext auch eine Verknüpfung mit der lexikalischen Umgebung des aufrufenden Ausführungskontexts, d.h. die in der äußeren lexikalischen Umgebung deklarierten Variablen (im obigen Beispiel sowohl a y b ) sind erhältlich bei ec .

Jede Funktion erzeugt einen Abschluss, da jede Funktion eine Verbindung zu ihrer äußeren lexikalischen Umgebung hat.

Beachten Sie, dass die Variablen selbst sind innerhalb eines Abschlusses sichtbar, no Kopien.

2610voto

Jacob Swartwood Punkte 4075

VORWORT: Diese Antwort wurde geschrieben, als die Frage lautete:

Wie der alte Albert sagte: "Wenn du es einem Sechsjährigen nicht erklären kannst, verstehst du es selbst nicht". Nun, ich habe versucht, einem 27-jährigen Freund JS-Schließungen zu erklären und bin völlig gescheitert.

Kann jemand bedenken, dass ich 6 bin und mich seltsamerweise für dieses Thema interessiere?

Ich bin mir ziemlich sicher, dass ich einer der Einzigen war, der versucht hat, die ursprüngliche Frage wörtlich zu nehmen. Seitdem hat sich die Frage mehrmals geändert, so dass meine Antwort jetzt vielleicht unglaublich albern und deplatziert erscheint. Hoffentlich bleibt der Grundgedanke der Geschichte für einige unterhaltsam.


Ich bin ein großer Fan von Analogien und Metaphern, wenn es darum geht, schwierige Konzepte zu erklären, also möchte ich es mit einer Geschichte versuchen.

Es war einmal eine Zeit:

Es gab eine Prinzessin...

function princess() {

Sie lebte in einer wunderbaren Welt voller Abenteuer. Sie traf ihren Märchenprinzen, ritt auf einem Einhorn durch die Welt, kämpfte gegen Drachen, begegnete sprechenden Tieren und vielen anderen fantastischen Dingen.

    var adventures = [];

    function princeCharming() { /* ... */ }

    var unicorn = { /* ... */ },
        dragons = [ /* ... */ ],
        squirrel = "Hello!";

    /* ... */

Aber sie würde immer wieder in ihre langweilige Welt der Hausarbeit und der Erwachsenen zurückkehren müssen.

    return {

Und sie erzählte ihnen oft von ihrem letzten großen Abenteuer als Prinzessin.

        story: function() {
            return adventures[adventures.length - 1];
        }
    };
}

Aber alles, was sie sehen würden, wäre ein kleines Mädchen...

var littleGirl = princess();

...Geschichten über Magie und Fantasie zu erzählen.

littleGirl.story();

Und obwohl die Erwachsenen von echten Prinzessinnen wussten, würden sie nie an Einhörner oder Drachen glauben, weil sie sie nie sehen konnten. Die Erwachsenen sagten, dass sie nur in der Fantasie des kleinen Mädchens existierten.

Aber wir kennen die wahre Wahrheit: das kleine Mädchen mit der Prinzessin im Herzen...

...ist in Wirklichkeit eine Prinzessin, die ein kleines Mädchen in sich trägt.

389 Stimmen

Ich liebe diese Erklärung, wirklich. Für diejenigen, die es lesen und nicht folgen können, ist die Analogie folgende: Die Funktion princess() ist ein komplexer Bereich, der private Daten enthält. Außerhalb der Funktion können die privaten Daten nicht eingesehen oder aufgerufen werden. Die Prinzessin behält die Einhörner, Drachen, Abenteuer usw. in ihrer Fantasie (private Daten) und die Erwachsenen können sie nicht selbst sehen. ABER die Phantasie der Prinzessin wird in der Schließung für die Kinder festgehalten. story() Funktion, die die einzige Schnittstelle ist, die die littleGirl Instanz in die Welt der Magie einführt.

4 Stimmen

Undefinierte Werte machen es schwieriger zu verstehen. Hier ist die wahre Geschichte jsfiddle.net/rjdx34k0/3

0 Stimmen

Oh schön, ich war so nah dran, eine Bearbeitung vorzunehmen, um das, was ich für das zusätzliche Leerzeichen am Anfang hielt, zu entfernen. Gute Arbeit, +1

813voto

dlaliberte Punkte 3182

Wenn wir die Frage ernst nehmen, sollten wir herausfinden, wozu ein typischer 6-Jähriger kognitiv fähig ist, auch wenn zugegebenermaßen einer, der sich für JavaScript interessiert, nicht so typisch ist.

Auf Entwicklung in der Kindheit: 5 bis 7 Jahre heißt es:

Ihr Kind wird in der Lage sein, Anweisungen in zwei Schritten zu befolgen. Wenn Sie Ihrem Kind zum Beispiel sagen: "Geh in die Küche und hol mir einen Müllsack", wird es sich diese Richtung merken können.

Anhand dieses Beispiels können wir Verschlüsse wie folgt erklären:

Die Küche ist ein Abschluss, der eine lokale Variable mit der Bezeichnung trashBags . In der Küche gibt es eine Funktion namens getTrashBag der einen Müllsack holt und ihn zurückbringt.

Wir können dies in JavaScript wie folgt kodieren:

function makeKitchen() {
  var trashBags = ['A', 'B', 'C']; // only 3 at first

  return {
    getTrashBag: function() {
      return trashBags.pop();
    }
  };
}

var kitchen = makeKitchen();

console.log(kitchen.getTrashBag()); // returns trash bag C
console.log(kitchen.getTrashBag()); // returns trash bag B
console.log(kitchen.getTrashBag()); // returns trash bag A

Weitere Punkte, die erklären, warum Schließungen interessant sind:

  • Jedes Mal makeKitchen() aufgerufen wird, wird ein neuer Abschluss mit einer eigenen separaten trashBags .
  • Le site trashBags Variable ist lokal im Inneren jeder Küche und nicht von außen zugänglich, aber die innere Funktion auf der getTrashBag Eigentum den Zugang dazu hat.
  • Jeder Funktionsaufruf erzeugt eine Closure, aber es gäbe keine Notwendigkeit, die Closure zu behalten, es sei denn, eine innere Funktion, die Zugriff auf das Innere der Closure hat, kann von außerhalb der Closure aufgerufen werden. Die Rückgabe des Objekts mit der getTrashBag Funktion macht das hier.

0 Stimmen

Wird der Abschluss buchstäblich erstellt, wenn makeKitchen() aufgerufen wird? Ich würde sagen, dass die Schließung durch die return Anweisung, die eine Referenz auf eine lokale Variable erhält trashBags bei der Erstellung des zurückzugebenden Funktionsobjekts. Ich denke, die Schließung ist Eigentum durch die anonyme Funktion, auf die die Eigenschaft getTrashBag des zurückgegebenen anonymen Objekts. (Ich habe in letzter Zeit Rust gelernt, und ich denke, dass die Eigentümerschaft eine Eigenschaft ist, die auch in anderen Sprachen hilft, Dinge zu klären).

0 Stimmen

@MikkoRantalainen, Sie haben Recht, dass die Closure um die innere Funktion nicht unbedingt erstellt wird, wenn die enthaltende Funktion aufgerufen wird, aber sie muss erstellt worden sein, wenn die Funktion zurückkehrt oder wenn die innere Funktion an einen anderen Kontext übergeben wird, was in diesem Beispiel nicht geschieht.

0 Stimmen

Ja, der Abschluss wird in dem Moment erstellt, in dem die anonyme Funktion wird erstellt bei der Definition der Eigenschaft getTrashBag des zurückzugebenden anonymen Objekts.

626voto

jondavidjohn Punkte 60651

Der Strohmann

Ich muss wissen, wie oft eine Schaltfläche angeklickt wurde, und bei jedem dritten Klick etwas tun...

Ziemlich offensichtliche Lösung

// Declare counter outside event handler's scope
var counter = 0;
var element = document.getElementById('button');

element.addEventListener("click", function() {
  // Increment outside counter
  counter++;

  if (counter === 3) {
    // Do something every third time
    console.log("Third time's the charm!");

    // Reset counter
    counter = 0;
  }
});

<button id="button">Click Me!</button>

Das funktioniert zwar, aber es greift in den äußeren Bereich ein, indem es eine Variable hinzufügt, deren einziger Zweck es ist, die Zählung zu verfolgen. In manchen Situationen wäre dies vorzuziehen, da Ihre äußere Anwendung möglicherweise Zugriff auf diese Informationen benötigt. Aber in diesem Fall ändern wir das Verhalten nur bei jedem dritten Klick, daher ist es besser, wenn wir diese Funktionalität in den Event-Handler einbinden .

Erwägen Sie diese Option

var element = document.getElementById('button');

element.addEventListener("click", (function() {
  // init the count to 0
  var count = 0;

  return function(e) { // <- This function becomes the click handler
    count++; //    and will retain access to the above `count`

    if (count === 3) {
      // Do something every third time
      console.log("Third time's the charm!");

      //Reset counter
      count = 0;
    }
  };
})());

<button id="button">Click Me!</button>

Hier sind einige Dinge zu beachten.

Im obigen Beispiel verwende ich das Schließverhalten von JavaScript. Dieses Verhalten ermöglicht es jeder Funktion, unbegrenzt auf den Bereich zuzugreifen, in dem sie erstellt wurde. Um dies praktisch anzuwenden, rufe ich sofort eine Funktion auf, die eine andere Funktion zurückgibt, und da die Funktion, die ich zurückgebe, Zugriff auf die interne Zählvariable hat (aufgrund des oben erläuterten Schließungsverhaltens), ergibt sich ein privater Bereich für die Verwendung durch die resultierende Funktion... Nicht so einfach? Verdünnen wir es...

Ein einfacher einzeiliger Abschluss

//          _______________________Immediately invoked______________________
//         |                                                                |
//         |        Scope retained for use      ___Returned as the____      |
//         |       only by returned function   |    value of func     |     |
//         |             |            |        |                      |     |
//         v             v            v        v                      v     v
var func = (function() { var a = 'val'; return function() { alert(a); }; })();

Alle Variablen außerhalb der zurückgegebenen Funktion sind für die zurückgegebene Funktion verfügbar, aber sie sind nicht direkt für das zurückgegebene Funktionsobjekt verfügbar...

func();  // Alerts "val"
func.a;  // Undefined

Verstehen Sie das? In unserem ersten Beispiel ist die Zählvariable in der Schließung enthalten und für den Event-Handler immer verfügbar, so dass sie ihren Zustand von Klick zu Klick beibehält.

Außerdem ist dieser Zustand der privaten Variablen vollständig zugänglich ist, sowohl für das Lesen als auch für die Zuweisung an seine privaten Variablen mit Gültigkeitsbereich.

Das war's. Jetzt haben Sie dieses Verhalten vollständig gekapselt.

Vollständiger Blog-Beitrag (einschließlich Überlegungen zu jQuery)

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X