Pytanie Jak działają zamknięcia JavaScript?


Jak wyjaśniłbyś zamknięcie JavaScriptu komuś ze znajomością pojęć, z których się składa (na przykład funkcje, zmienne i tym podobne), ale nie rozumie samych zamknięć?

widziałem przykład Schematu podane na Wikipedii, ale niestety to nie pomogło.


7654


pochodzenie


Mój problem z tymi i wieloma odpowiedziami polega na tym, że podchodzą do niego z perspektywy abstrakcyjnej, teoretycznej, a nie zaczynają od wyjaśnienia, po prostu, dlaczego zamykanie jest konieczne w JavaScript i praktycznych sytuacjach, w których z nich korzystasz. Kończysz z tl; dr artykułem, który musisz przemycić, cały czas myśląc "ale dlaczego?". Po prostu zacznę od: zamknięć są schludny sposób radzenia sobie z następujących dwóch rzeczywistości JavaScript:. zakres jest na poziomie funkcji, a nie na poziomie bloku i, b. wiele z tego, co robisz w praktyce w JavaScript, jest sterowanych asynchronicznie / zdarzeniami. - Jeremy Burton
@Redsandro Po pierwsze, sprawia, że ​​kod sterowany zdarzeniami jest o wiele łatwiejszy do napisania. Mogę uruchomić funkcję, gdy strona ładuje się, aby określić szczegóły dotyczące HTML lub dostępnych funkcji. Mogę zdefiniować i ustawić funkcję obsługi w tej funkcji i wszystkie informacje kontekstowe są dostępne za każdym razem, gdy wywoływany jest moduł obsługi bez konieczności ponownego wysyłania zapytania. Rozwiąż problem raz, użyj go ponownie na każdej stronie, na której ten handler jest potrzebny, ze zredukowanym narzutem na ponowną inwokację obsługi. Czy widzisz, że te same dane są ponownie zamapowane dwukrotnie w języku, który ich nie ma? Zamknięcia sprawiają, że o wiele łatwiej jest tego uniknąć. - Erik Reppen
Dla programistów Java krótka odpowiedź brzmi, że jest to odpowiednik funkcji wewnętrznej klasy. Klasa wewnętrzna również zawiera niejawny wskaźnik do instancji klasy zewnętrznej i jest używana do tego samego celu (to znaczy do tworzenia procedur obsługi zdarzeń). - Boris van Schooten
Zrozumiałem o wiele lepiej stąd: javascriptissexy.com/understand-javascript-closures-with-ease. Wciąż potrzebowałeś zamknięcie w sprawie zamknięcia po przeczytaniu innych odpowiedzi. :) - Akhoy
Ten przykład praktyczny okazał się bardzo przydatny: youtube.com/watch?v=w1s9PgtEoJs - Abhi


Odpowiedzi:


Zamknięcia JavaScript dla początkujących

Zgłoszony przez Morrisa na wtorek, 2006-02-21 10:19. Edytowany przez społeczność od.

Zamknięcia nie są magiczne

Ta strona wyjaśnia, w jaki sposób programista może je zrozumieć - za pomocą działającego kodu JavaScript. Nie jest przeznaczony dla guru ani programistów funkcjonalnych.

Zamknięcia są nietrudny aby zrozumieć, kiedy już podstawowa koncepcja jest grokowana. Jednak nie można tego zrozumieć, czytając jakiekolwiek akademickie gazety lub akademickie informacje na ich temat!

Ten artykuł jest przeznaczony dla programistów, którzy mają pewne doświadczenie w programowaniu w głównym języku i którzy mogą czytać następującą funkcję JavaScript:

function sayHello(name) {
  var text = 'Hello ' + name;
  var say = function() { console.log(text); }
  say();
}
sayHello('Joe');

Przykład zamknięcia

Dwa podsumowania jednego zdania:

  • Zamknięcie jest jednym ze sposobów wspierania pierwszorzędne funkcje; jest to wyrażenie, które może odwoływać się do zmiennych w swoim zasięgu (kiedy zostało zadeklarowane po raz pierwszy), być przypisane do zmiennej, przekazane jako argument do funkcji lub zwrócone jako wynik funkcji.

  • Lub zamknięcie jest ramką stosu, która jest alokowana, gdy funkcja rozpoczyna jej wykonywanie, oraz nie uwolniony po powrocie funkcji (tak jakby na stosie została przydzielona "ramka stosu" zamiast stosu!).

Poniższy kod zwraca odwołanie do funkcji:

function sayHello2(name) {
  var text = 'Hello ' + name; // Local variable
  var say = function() { console.log(text); }
  return say;
}
var say2 = sayHello2('Bob');
say2(); // logs "Hello Bob"

Większość programistów JavaScript zrozumie, jak odniesienie do funkcji jest zwracane do zmiennej (say2) w powyższym kodzie. Jeśli tego nie zrobisz, musisz na to spojrzeć, zanim nauczysz się zamknąć. Programista używający C pomyślałby o funkcji zwracającej wskaźnik do funkcji oraz o zmiennych say i say2 każdy był wskaźnikiem funkcji.

Istnieje zasadnicza różnica między wskaźnikiem C a funkcją i odwołaniem do funkcji JavaScript. W języku JavaScript można myśleć o zmiennej odwołania do funkcji jako o wskaźniku do funkcji także jako ukryty wskaźnik do zamknięcia.

Powyższy kod ma zamknięcie, ponieważ funkcja anonimowa function() { console.log(text); } jest zadeklarowane wewnątrz inna funkcja, sayHello2() w tym przykładzie. W JavaScript, jeśli używasz function w innej funkcji, tworzysz zamknięcie.

W C i większości innych popularnych języków, po funkcja zwraca, wszystkie zmienne lokalne nie są już dostępne, ponieważ ramka stosu jest zniszczona.

W JavaScript, jeśli deklarujesz funkcję w ramach innej funkcji, zmienne lokalne mogą pozostać dostępne po powrocie z funkcji, którą wywołałeś. Zostało to wykazane powyżej, ponieważ nazywamy tę funkcję say2()po powrocie z sayHello2(). Zauważ, że kod, który nazywamy, odwołuje się do zmiennej text, który był zmienna lokalna funkcji sayHello2().

function() { console.log(text); } // Output of say2.toString();

Patrząc na wynik say2.toString(), widzimy, że kod odnosi się do zmiennej text. Funkcja anonimowa może odwoływać się text który trzyma wartość 'Hello Bob' ponieważ zmienne lokalne z sayHello2() są trzymane w zamknięciu.

Magia polega na tym, że w JavaScript odwołanie do funkcji ma również tajne odniesienie do zamknięcia, w którym zostało utworzone - podobnie jak delegaty są wskaźnikiem metody oraz tajnym odwołaniem do obiektu.

Więcej przykładów

Z jakiegoś powodu zamknięcia wydają się bardzo trudne do zrozumienia, gdy czytasz o nich, ale kiedy widzisz kilka przykładów, staje się jasne, jak działają (zajęło mi to trochę czasu). Polecam dokładną analizę przykładów, aż zrozumiesz, jak działają. Jeśli zaczniesz używać zamknięć bez pełnego zrozumienia ich działania, wkrótce utworzysz kilka bardzo dziwnych błędów!

Przykład 3

Ten przykład pokazuje, że zmienne lokalne nie są kopiowane - są przechowywane przez odniesienie. Jest to trochę tak, jak przechowywanie klatki stosu w pamięci po wyjściu zewnętrznej funkcji!

function say667() {
  // Local variable that ends up within closure
  var num = 42;
  var say = function() { console.log(num); }
  num++;
  return say;
}
var sayNumber = say667();
sayNumber(); // logs 43

Przykład 4

Wszystkie trzy globalne funkcje mają wspólne odniesienie do podobnie zamknięcie, ponieważ wszystkie są zadeklarowane w jednym wywołaniu setupSomeGlobals().

var gLogNumber, gIncreaseNumber, gSetNumber;
function setupSomeGlobals() {
  // Local variable that ends up within closure
  var num = 42;
  // Store some references to functions as global variables
  gLogNumber = function() { console.log(num); }
  gIncreaseNumber = function() { num++; }
  gSetNumber = function(x) { num = x; }
}

setupSomeGlobals();
gIncreaseNumber();
gLogNumber(); // 43
gSetNumber(5);
gLogNumber(); // 5

var oldLog = gLogNumber;

setupSomeGlobals();
gLogNumber(); // 42

oldLog() // 5

Te trzy funkcje mają wspólny dostęp do tego samego zamknięcia - lokalne zmienne setupSomeGlobals() kiedy zdefiniowano trzy funkcje.

Zauważ, że w powyższym przykładzie, jeśli zadzwonisz setupSomeGlobals() znowu, wtedy powstaje nowe zamknięcie (stack-frame!). Stary gLogNumber, gIncreaseNumber, gSetNumber zmienne są zastępowane Nowy funkcje, które mają nowe zamknięcie. (W JavaScript, kiedykolwiek deklarujesz funkcję wewnątrz innej funkcji, funkcje wewnętrzne są / są odtwarzane ponownie każdy czas wywołania funkcji zewnętrznej.)

Przykład 5

Ten przykład pokazuje, że zamknięcie zawiera wszelkie zmienne lokalne, które zostały zadeklarowane w zewnętrznej funkcji przed jej wyjściem. Zwróć uwagę, że zmienna alice jest faktycznie zadeklarowana po funkcji anonimowej. Funkcja anonimowa jest zadeklarowana jako pierwsza; a kiedy ta funkcja jest wywoływana, może uzyskać dostęp do alice zmienna, ponieważ alice jest w tym samym zakresie (JavaScript robi zmienne podnoszenie). Również sayAlice()() bezpośrednio wywołuje referencję funkcji zwróconą z sayAlice() - jest dokładnie taki sam jak poprzednio, ale bez zmiennej tymczasowej.

function sayAlice() {
    var say = function() { console.log(alice); }
    // Local variable that ends up within closure
    var alice = 'Hello Alice';
    return say;
}
sayAlice()();// logs "Hello Alice"

Tricky: zauważ również, że say zmienna znajduje się również w zamknięciu i można do niej uzyskać dostęp za pomocą dowolnej innej funkcji, która może zostać zadeklarowana w ramach sayAlice()lub może być dostępny rekurencyjnie w ramach funkcji wewnętrznej.

Przykład 6

Ten jest prawdziwy dla wielu ludzi, więc musisz go zrozumieć. Zachowaj ostrożność, jeśli definiujesz funkcję w pętli: zmienne lokalne z zamknięcia mogą nie działać tak, jak mogłoby się wydawać.

Aby zrozumieć ten przykład, musisz zrozumieć funkcję "zmiennego podnoszenia" w Javascript.

function buildList(list) {
    var result = [];
    for (var i = 0; i < list.length; i++) {
        var item = 'item' + i;
        result.push( function() {console.log(item + ' ' + list[i])} );
    }
    return result;
}

function testList() {
    var fnlist = buildList([1,2,3]);
    // Using j only to help prevent confusion -- could use i.
    for (var j = 0; j < fnlist.length; j++) {
        fnlist[j]();
    }
}

 testList() //logs "item2 undefined" 3 times

Linia result.push( function() {console.log(item + ' ' + list[i])} dodaje odwołanie do funkcji anonimowej trzy razy do tablicy wyników. Jeśli nie znasz się na anonimowe funkcje, pomyśl o tym tak:

pointer = function() {console.log(item + ' ' + list[i])};
result.push(pointer);

Zwróć uwagę, że po uruchomieniu przykładu "item2 undefined" jest zalogowany trzy razy! Jest tak, ponieważ podobnie jak w poprzednich przykładach, dla zmiennych lokalnych istnieje tylko jedno zamknięcie buildList (które są result, i i item). Kiedy anonimowe funkcje są wywoływane na linii fnlist[j](); wszystkie używają tego samego pojedynczego zamknięcia i używają bieżącej wartości dla i i item w ramach tego jednego zamknięcia (gdzie i ma wartość 3 ponieważ pętla została zakończona, i item ma wartość 'item2'). Zauważ, że indeksujemy od 0 stąd item ma wartość item2. I ++ zwiększy się i do wartości 3.

Pomocne może być zobaczenie, co się dzieje, gdy deklaracja zmiennej na poziomie bloku item jest używany (przez let słowo kluczowe) zamiast deklaracji zmiennej o zakresie funkcji za pośrednictwem var słowo kluczowe. Jeśli ta zmiana zostanie wprowadzona, to każda anonimowa funkcja w tablicy result ma własne zamknięcie; po uruchomieniu przykładu dane wyjściowe są następujące:

item0 undefined
item1 undefined
item2 undefined

Jeśli zmienna i jest również zdefiniowany przy użyciu let zamiast var, a wynik to:

item0 1
item1 2
item2 3

Przykład 7

W tym ostatnim przykładzie każde wywołanie funkcji głównej tworzy oddzielne zamknięcie.

function newClosure(someNum, someRef) {
    // Local variables that end up within closure
    var num = someNum;
    var anArray = [1,2,3];
    var ref = someRef;
    return function(x) {
        num += x;
        anArray.push(num);
        console.log('num: ' + num +
            '; anArray: ' + anArray.toString() +
            '; ref.someVar: ' + ref.someVar + ';');
      }
}
obj = {someVar: 4};
fn1 = newClosure(4, obj);
fn2 = newClosure(5, obj);
fn1(1); // num: 5; anArray: 1,2,3,5; ref.someVar: 4;
fn2(1); // num: 6; anArray: 1,2,3,6; ref.someVar: 4;
obj.someVar++;
fn1(2); // num: 7; anArray: 1,2,3,5,7; ref.someVar: 5;
fn2(2); // num: 8; anArray: 1,2,3,6,8; ref.someVar: 5;

Podsumowanie

Jeśli wszystko wydaje się całkowicie niejasne, najlepiej jest bawić się przykładami. Przeczytanie wyjaśnienia jest znacznie trudniejsze niż zrozumienie przykładów. Moje wyjaśnienia dotyczące zamknięć i ramek stosu itp. Nie są poprawne technicznie - są to wulgaryzmy uproszczone, które mają pomóc w zrozumieniu. Po podstawowym założeniu jest grokked, można odebrać szczegóły później.

Ostateczne punkty:

  • Kiedykolwiek używasz function w innej funkcji stosuje się zamknięcie.
  • Kiedykolwiek używasz eval() wewnątrz funkcji stosuje się zamknięcie. Twój tekst eval może odwoływać się do lokalnych zmiennych funkcji i wewnątrz eval można nawet tworzyć nowe zmienne lokalne za pomocą eval('var foo = …')
  • Kiedy używasz new Function(…) ( Konstruktor funkcji) wewnątrz funkcji, nie tworzy zamknięcia. (Nowa funkcja nie może odwoływać się do zmiennych lokalnych funkcji zewnętrznej).
  • Zamknięcie w JavaScript jest jak przechowywanie kopii wszystkich zmiennych lokalnych, tak jak w momencie wyjścia z funkcji.
  • Prawdopodobnie najlepiej jest myśleć, że zamknięcie jest zawsze tworzone tylko z wpisu do funkcji, a zmienne lokalne są dodawane do tego zamknięcia.
  • Nowy zestaw zmiennych lokalnych jest przechowywany za każdym razem, gdy wywoływana jest funkcja z zamknięciem (biorąc pod uwagę, że funkcja zawiera deklarację funkcji wewnątrz niej i zwracane jest odwołanie do tej funkcji wewnętrznej lub jest ona dla niej zachowywana w pewien sposób ).
  • Dwie funkcje mogą wyglądać tak, jakby miały ten sam tekst źródłowy, ale mają zupełnie inne zachowanie ze względu na ich "ukryte" zamknięcie. Nie sądzę, że kod JavaScript może rzeczywiście dowiedzieć się, czy odwołanie do funkcji ma zamknięcie czy nie.
  • Jeśli próbujesz wykonać dowolne dynamiczne modyfikacje kodu źródłowego (na przykład: myFunction = Function(myFunction.toString().replace(/Hello/,'Hola'));), to nie zadziała, jeśli myFunction to zamknięcie (oczywiście, nigdy byś nawet nie pomyślał o tym, żeby w czasie wykonywania zmienić kodowanie łańcuchów kodu źródłowego, ale ...).
  • Możliwe jest uzyskanie deklaracji funkcji w deklaracjach funkcji w ramach funkcji - i można uzyskać zamknięcia na więcej niż jednym poziomie.
  • Myślę, że normalnie zamknięcie jest terminem zarówno dla funkcji, jak i zmiennych, które są przechwytywane. Zauważ, że nie używam tej definicji w tym artykule!
  • Podejrzewam, że zamknięcia w JavaScript różnią się od tych, które zwykle występują w językach funkcjonalnych.

Spinki do mankietów

Dzięki

Jeśli masz właśnie nauczone zamknięcia (tutaj lub gdziekolwiek indziej!), następnie jestem zainteresowany wszelkimi informacjami zwrotnymi od ciebie o wszelkich zmianach, które możesz sugerować, które mogą sprawić, że ten artykuł będzie bardziej przejrzysty. Wyślij wiadomość e-mail na adres morrisjohns.com (morris_closure @). Pamiętaj, że nie jestem guru w JavaScript - ani w zamknięciach.


Oryginalny post Morris można znaleźć w Archiwum internetowe.


6173



Znakomity. Szczególnie uwielbiam: "Zamknięcie w JavaScript jest jak przechowywanie kopii wszystkich zmiennych lokalnych, tak jak wtedy, gdy funkcja została zakończona." - e-satis
@ e-satis - genialne, jak może się wydawać, "kopia wszystkich zmiennych lokalnych, tak jak to było w momencie wyjścia z funkcji" wprowadza w błąd. Sugeruje to, że wartości zmiennych są kopiowane, ale tak naprawdę to zbiór samych zmiennych, który nie zmienia się po wywołaniu funkcji (z wyjątkiem "eval" może: blog.rakeshpai.me/2008/10/...). Sugeruje to, że funkcja musi powrócić przed utworzeniem zamknięcia, ale nie musi powrócić, zanim zamknięcie może zostać wykorzystane jako zamknięcie. - dlaliberte
Brzmi to ładnie: "Zamknięcie w JavaScript jest jak przechowywanie kopii wszystkich zmiennych lokalnych, tak jak wtedy, gdy funkcja została zakończona." Jest to jednak mylące z kilku powodów. (1) Wywołanie funkcji nie musi kończyć się w celu utworzenia zamknięcia. (2) To nie jest kopia wartości zmiennych lokalnych, ale same zmienne. (3) Nie określa, kto ma dostęp do tych zmiennych. - dlaliberte
Przykład 5 pokazuje "gotcha", gdzie kod nie działa zgodnie z przeznaczeniem. Ale nie pokazuje, jak to naprawić. Ta inna odpowiedź pokazuje sposób na zrobienie tego. - Matt
Podoba mi się, jak ten post zaczyna się od dużych pogrubionych liter mówiących "Closures Are Not Magic" i kończy swój pierwszy przykład "Magia polega na tym, że w JavaScript odwołanie do funkcji ma również tajne odniesienie do zamknięcia, w którym zostało utworzone". - Andrew Macheret


Kiedy tylko zobaczysz słowo kluczowe function w innej funkcji, funkcja wewnętrzna ma dostęp do zmiennych w funkcji zewnętrznej.

function foo(x) {
  var tmp = 3;

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

  bar(10);
}

foo(2);

To zawsze będzie rejestrować 16, ponieważ bar może uzyskać dostęp do x który został zdefiniowany jako argument dla fooi może również uzyskać dostęp tmp od foo.

Że jest zamknięcie. Funkcja nie musi powrót aby zostać nazwanym zamknięciem. Po prostu dostęp do zmiennych poza twoim bezpośrednim zasięgiem leksykalnym tworzy zamknięcie.

function foo(x) {
  var tmp = 3;

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

var bar = foo(2); // bar is now a closure.
bar(10);

Powyższa funkcja będzie również rejestrować 16, ponieważ bar nadal mogę się odwoływać x i tmp, chociaż nie jest już bezpośrednio w zasięgu.

Jednak od tego czasu tmp wciąż wisi w środku barjest zamykany, również jest zwiększany. Będzie on zwiększany za każdym razem, gdy zadzwonisz bar.

Najprostszym przykładem zamknięcia jest to:

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

Gdy wywoływana jest funkcja JavaScript, tworzony jest nowy kontekst wykonania. Wraz z argumentami funkcji i obiektem nadrzędnym ten kontekst wykonania otrzymuje również wszystkie zmienne zadeklarowane poza nim (w powyższym przykładzie zarówno "a" i "b").

Można utworzyć więcej niż jedną funkcję zamknięcia, zwracając listę lub ustawiając je na zmienne globalne. Wszystkie te będą odnosić się do podobnie  x i to samo tmp, nie robią swoich własnych kopii.

Tutaj liczba x jest liczbą dosłowną. Podobnie jak w przypadku innych literałów w JavaScript, kiedy foo nazywa się, liczba x jest skopiowane w foo jako jego argument x.

Z drugiej strony JavaScript zawsze używa odwołań do obsługi obiektów. Jeśli tak, zadzwoniłeś fooz obiektem, zamknięcie, które zwróci, będzie odniesienie ten oryginalny obiekt!

function foo(x) {
  var tmp = 3;

  return function (y) {
    console.log(x + y + tmp);
    x.memb = x.memb ? x.memb + 1 : 1;
    console.log(x.memb);
  }
}

var age = new Number(2);
var bar = foo(age); // bar is now a closure referencing age.
bar(10);

Zgodnie z oczekiwaniami każde wezwanie do bar(10) będzie wzrastał x.memb. Czego można nie oczekiwać, to jest to x odnosi się po prostu do tego samego obiektu, co age zmienna! Po kilku telefonach do bar, age.memb będzie 2! Odwołania te są podstawą wycieków pamięci z obiektami HTML.


3808



@ Feeela: Tak, każda funkcja JS tworzy zamknięcie. Zmienne, do których nie odwołujemy się prawdopodobnie zostaną zakwalifikowane do zbierania śmieci w nowoczesnych silnikach JS, ale nie zmienia to faktu, że podczas tworzenia kontekstu wykonawczego ten kontekst ma odniesienie do otaczającego kontekstu wykonywania i jego zmiennych, oraz ta funkcja jest obiektem, który może zostać przeniesiony do innego zakresu zmiennych, zachowując pierwotne odniesienie. To jest zamknięcie.
@Ali Właśnie odkryłem, że podany przeze mnie jsFiddle faktycznie niczego nie udowadnia, ponieważ delete zawiedzie. Niemniej jednak środowisko leksykalne, które funkcja będzie nosiła jako [[Zakres]] (i ostatecznie używane jako podstawa dla swojego własnego środowiska leksykalnego, gdy zostanie wywołane) jest określane, kiedy wykonywana jest instrukcja definiująca funkcję. Oznacza to, że funkcja jest zamykanie CAŁKOWITEJ treści zakresu wykonawczego, niezależnie od tego, do których wartości faktycznie się odnosi i czy wymyka się zakresowi. Proszę spojrzeć na rozdziały 13.2 i 10 w specyfikacja - Asad Saeeduddin
To była dobra odpowiedź, dopóki nie próbowała wyjaśnić podstawowych typów i odniesień. Jest to całkowicie błędne i mówi o kopiowaniu literałów, które tak naprawdę nie mają nic wspólnego z niczym. - Ry-♦
Zamknięcia są odpowiedzią JavaScriptu na oparte na klasach programowanie zorientowane obiektowo. JS nie jest oparty na klasach, więc trzeba znaleźć inny sposób implementacji pewnych rzeczy, których nie można by inaczej zaimplementować. - Barth Zalewski
to powinna być zaakceptowana odpowiedź. Magia nigdy nie dzieje się w funkcji wewnętrznej. Zdarza się, gdy przypisujemy zewnętrzną funkcję do zmiennej. Stwarza to nowy kontekst wykonania dla funkcji wewnętrznej, więc "zmienna prywatna" może być akumulowana. Oczywiście może, ponieważ zmienna, do której przypisano zewnętrzną funkcję, zachowała kontekst. Pierwsza odpowiedź sprawia, że ​​całość jest bardziej skomplikowana, nie wyjaśniając, co naprawdę się tam dzieje. - Albert Gao


PRZEDMOWA: ta odpowiedź została napisana, gdy pytanie brzmiało:

Podobnie jak stary Albert powiedział: "Jeśli nie możesz wyjaśnić tego sześciolatkowi, naprawdę nie rozumiesz tego sam." Cóż, próbowałem wyjaśnić zamknięcie JS 27-letniemu przyjacielowi i kompletnie mu się nie udało.

Czy ktokolwiek może uznać, że mam 6 lat i dziwnie interesuję się tym tematem?

Jestem prawie pewien, że byłem jedną z niewielu osób, które próbowały dosłownie podejść do pytania. Od tego czasu pytanie zmutowało kilka razy, więc moja odpowiedź może teraz wydawać się niesamowicie głupia i nie na miejscu. Mam nadzieję, że ogólny pomysł na tę historię jest dla niektórych zabawą.


Jestem wielkim fanem analogii i metafory przy tłumaczeniu trudnych pojęć, więc pozwól mi spróbować swoich sił w opowiadaniu.

Pewnego razu:

Była księżniczka ...

function princess() {

Mieszkała w cudownym świecie pełnym przygód. Spotkała swojego księcia z bajki, krążyła po świecie na jednorożcu, walczyła ze smokami, napotykała rozmawiające zwierzęta i wiele innych fantastycznych rzeczy.

    var adventures = [];

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

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

    /* ... */

Ale zawsze będzie musiała powrócić do nudnego świata obowiązków i dorosłych.

    return {

I często opowiadała im o swojej najnowszej niesamowitej przygodzie jako księżniczki.

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

Ale wszystko, co zobaczą, to mała dziewczynka ...

var littleGirl = princess();

... opowiadając historie o magii i fantazji.

littleGirl.story();

I chociaż dorośli wiedzieli o prawdziwych księżniczkach, nigdy nie uwierzyliby w jednorożce czy smoki, ponieważ nigdy nie mogliby ich zobaczyć. Dorośli mówili, że istnieją tylko w wyobraźni małej dziewczynki.

Ale znamy prawdziwą prawdę; że mała dziewczynka z księżniczką w środku ...

... to naprawdę księżniczka z małą dziewczynką w środku.


2253



Uwielbiam to wyjaśnienie, naprawdę. Dla tych, którzy ją czytają i nie podążają, analogia jest następująca: funkcja princess () to złożony zakres zawierający prywatne dane. Poza funkcją prywatne dane nie są widoczne ani dostępne. Księżniczka trzyma w swojej wyobraźni jednorożce, smoki, przygody itp. (Dane prywatne), a dorośli nie widzą ich dla siebie. ALE wyobraźnia księżniczki została uchwycona w zamknięciu dla story() funkcja, która jest jedynym interfejsem littleGirl instancja ujawnia się w świecie magii. - Patrick M
Więc tu story jest zamknięciem, ale kod był var story = function() {}; return story; następnie littleGirl byłby zamknięciem. Przynajmniej to wrażenie, od którego dostaję Wykorzystanie przez MDN "prywatnych" metod z zamknięciami: "Tymi trzema funkcjami publicznymi są zamknięte, które mają wspólne środowisko". - icc97
@ icc97, tak, story jest zamknięciem odnoszącym się do środowiska udostępnionego w ramach princess. princess jest także innym ukryty zamknięcie, tj princess i littleGirl podzieliłoby się odniesieniem do parents tablica, która będzie istnieć w środowisku / zasięgu, gdzie littleGirl istnieje i princess definiuje. - Jacob Swartwood
@BenjaminKrupp Dodałem wyraźny komentarz do kodu, aby pokazać / sugerować, że w ciele jest więcej operacji princess niż to, co napisano. Niestety ta historia jest teraz nieco nie na miejscu w tym wątku. Pierwotnie pytanie dotyczyło "wyjaśnienia zamknięcia JavaScriptu do starego 5 lat"; moja odpowiedź była jedyną, która nawet próbowała to zrobić. Nie wątpię, że zawiodłoby to nieszczęśliwie, ale przynajmniej ta odpowiedź mogła mieć szansę na zainteresowanie pięcioletniego chłopca. - Jacob Swartwood
Właściwie dla mnie to miało sens. I muszę przyznać, że ostatecznie zrozumienie zamknięcia JS za pomocą opowieści o księżniczkach i przygodach sprawia, że ​​czuję się dziwnie. - Crystallize


Poważnie podchodząc do tego pytania, powinniśmy się dowiedzieć, co typowy sześciolatek potrafi poznawać, choć wprawdzie osoba zainteresowana JavaScriptem nie jest taka typowa.

Na Rozwój dzieciństwa: od 5 do 7 lat  to mówi:

Twoje dziecko będzie mogło podążać dwuetapowymi wskazówkami. Na przykład, jeśli powiesz swojemu dziecku: "Idź do kuchni i przynieś mi torbę na śmieci", będą mogli zapamiętać ten kierunek.

Możemy użyć tego przykładu, aby wyjaśnić zamknięcia, jak następuje:

Kuchnia jest zamknięciem, które ma zmienną lokalną, zwaną trashBags. W kuchni jest funkcja getTrashBag który dostaje jedną torbę na śmieci i zwraca ją.

Możemy kodować to w JavaScript tak:

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

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

var kitchen = makeKitchen();

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

Dalsze punkty wyjaśniające, dlaczego zamknięcia są interesujące:

  • Za każdym razem makeKitchen() jest nazywany, nowe zamknięcie jest tworzone z własnym oddzielnym trashBags.
  • The trashBags zmienna jest lokalna do wnętrza każdej kuchni i nie jest dostępna na zewnątrz, ale wewnętrzna funkcja na getTrashBagwłasność ma do niego dostęp.
  • Każde wywołanie funkcji tworzy zamknięcie, ale nie byłoby potrzeby utrzymywania zamknięcia wokół, chyba że wewnętrzna funkcja, która ma dostęp do wnętrza zamknięcia, może być wywołana z zewnątrz zamknięcia. Zwracanie obiektu za pomocą getTrashBag funkcja robi to tutaj.

693



Właściwie, myląco, funkcja makeKitchen połączenie jest faktycznym zamknięciem, a nie obiektem kuchennym, który powraca. - dlaliberte
Przechodząc przez innych, znalazłem tę odpowiedź jako najprostszy sposób wyjaśnienia, co i dlaczego zamknięcie. - Chetabahana
Za dużo menu i przystawka, za mało mięsa i ziemniaków. Możesz poprawić tę odpowiedź za pomocą jednego krótkiego zdania, na przykład: "Zamknięcie jest zamkniętym kontekstem funkcji, z powodu braku jakiegokolwiek mechanizmu określania zakresu dostarczonego przez klasy". - Jacob


The Straw Man

Muszę wiedzieć, ile razy kliknięto przycisk i zrobić coś przy każdym trzecim kliknięciu ...

Dość oczywiste rozwiązanie

// 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>

Teraz to zadziała, ale wkracza w zewnętrzny zakres, dodając zmienną, której jedynym celem jest śledzenie liczby. W niektórych sytuacjach byłoby to lepsze, ponieważ zewnętrzna aplikacja może potrzebować dostępu do tych informacji. Ale w tym przypadku zmieniamy tylko zachowanie co trzeciego kliknięcia, więc lepiej jest zamknij tę funkcjonalność wewnątrz obsługi zdarzenia.

Rozważ tę opcję

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>

Zwróć uwagę na kilka rzeczy tutaj.

W powyższym przykładzie używam zachowania zamknięcia JavaScript. To zachowanie umożliwia dowolnej funkcji dostęp do zakresu, w którym został utworzony, w nieskończoność. Aby praktycznie zastosować to, natychmiast wywołuję funkcję, która zwraca inną funkcję, a ponieważ funkcja, którą wracam ma dostęp do wewnętrznej zmiennej licznika (z powodu wyjaśnionego powyżej zachowania zamknięcia), daje to prywatny zakres do wykorzystania przez wynikową funkcję. funkcja ... Nie takie proste? Rozcieńczmy to ...

Proste zamknięcie jednej linii

//          _______________________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); }; })();

Wszystkie zmienne poza zwróconą funkcją są dostępne dla zwróconej funkcji, ale nie są one bezpośrednio dostępne dla zwróconego obiektu funkcji ...

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

Zdobyć? Tak więc w naszym głównym przykładzie zmienna licznika jest zawarta w zamknięciu i zawsze dostępna dla obsługi zdarzenia, więc zachowuje swój stan z kliknięcia.

Również ten prywatny stan zmiennych to całkowicie dostępne, zarówno dla odczytów, jak i przypisywania do jego prywatnych zmiennych zakresu.

Proszę bardzo; w pełni zachowujesz to zachowanie.

Pełny post w blogu (w tym względy jQuery)


527



Nie zgadzam się z twoją definicją tego, co to jest zamknięcie. Nie ma powodu, aby musiał się on samozwaszyć. Jest to również nieco uproszczone (i niedokładne) stwierdzenie, że musi zostać "zwrócony" (dużo dyskusji na ten temat w komentarzach do najwyższej odpowiedzi na to pytanie) - James Montagne
@ James nawet jeśli się nie zgadzasz, jego przykład (i cały post) jest jednym z najlepszych, jakie widziałem. Chociaż pytanie nie jest stare i rozwiązane, całkowicie zasługuję na +1. - e-satis
"Muszę wiedzieć, ile razy kliknięto przycisk, i zrobić coś przy każdym trzecim kliknięciu ..." TO zwróciło moją uwagę. Przypadek użycia i rozwiązanie pokazujące, że zamknięcie nie jest tak tajemniczą rzeczą i że wielu z nas pisze je, ale nie zna dokładnie oficjalnej nazwy. - Chris22
Dobry przykład, ponieważ pokazuje, że "count" w drugim przykładzie zachowuje wartość "count" i nie resetuje się do 0 za każdym razem, gdy kliknie się "element". Bardzo informujące! - Adam
+1 dla zachowanie zamknięcia. Czy możemy ograniczyć? zachowanie zamknięcia do Funkcje w javascript czy ta koncepcja może być również zastosowana do innych struktur języka? - Dziamid


Zamknięcia są trudne do wytłumaczenia, ponieważ są używane do wykonywania pewnych czynności, które i tak każdy intuicyjnie spodziewa się pracować. Znajduję najlepszy sposób na ich wyjaśnienie (i sposób, w jaki ja dowiedzieli się, co robią) to wyobrazić sobie sytuację bez nich:

    var bind = function(x) {
        return function(y) { return x + y; };
    }
    
    var plus5 = bind(5);
    console.log(plus5(3));

Co by się stało, gdyby JavaScript nie zrobił znasz zamknięcia? Po prostu zamień wywołanie w ostatnim wierszu przez jego treść metody (która w zasadzie jest tym, do czego służą wywołania funkcji) i otrzymasz:

console.log(x + 3);

Teraz, gdzie jest definicja x? Nie zdefiniowaliśmy go w bieżącym zakresie. Jedynym rozwiązaniem jest pozwolić plus5  nieść jego zakres (lub raczej zakres jego rodzica) wokół. Tą drogą, x jest dobrze zdefiniowany i jest związany z wartością 5.


445



Zgadzam się. Nadanie funkcji znaczących nazw zamiast tradycyjnych "foobarowych" również bardzo mi pomaga. Semantyka się liczy. - Ishmael
więc w pseudojęzyku jest zasadniczo podobny alert(x+3, where x = 5). The where x = 5jest zamknięciem. Czy mam rację? - Jus12
@ Jus12: dokładnie. Za kulisami zamknięcie to tylko trochę przestrzeni, w której przechowywane są bieżące wartości zmiennych ("wiązania"), tak jak w twoim przykładzie. - Konrad Rudolph
To jest właśnie ten przykład, który wprowadza wielu ludzi w błąd, myśląc, że jest to wartości które są używane w zwracanej funkcji, a nie sama zmienna zmienna. Jeśli zostałby zmieniony na "return x + = y", lub lepiej zarówno to, jak i inną funkcję "x * = y", wówczas jasne byłoby, że nic nie jest kopiowane. Dla osób, które są używane do układania ramek, wyobraź sobie, używając zamiast tego ramek sterty, które mogą istnieć po powrocie funkcji. - Matt
@Matt Nie zgadzam się. Przykładem jest nie powinien wyczerpująco dokumentować wszystkie właściwości. To ma być redukcyjny i ilustrują istotną cechę koncepcji. OP poprosił o proste wyjaśnienie ("dla sześciolatka"). Weź zaakceptowaną odpowiedź: To całkowicie zawiedzie dostarczając zwięzłego wyjaśnienia, właśnie dlatego, że stara się być wyczerpująca. (Zgadzam się z tobą, że jest to ważna własność JavaScript, która wiąże się przez odniesienie, a nie przez wartość ... ale znowu, udane wyjaśnienie to takie, które zmniejsza się do absolutnego minimum.) - Konrad Rudolph


Jest to próba wyjaśnienia kilku (możliwych) nieporozumień dotyczących zamknięć pojawiających się w niektórych innych odpowiedziach.

  • Zamknięcie nie powstaje tylko wtedy, gdy zwrócisz wewnętrzną funkcję. W rzeczywistości funkcja otaczająca w ogóle nie musi wracać w celu jego zamknięcia. Zamiast tego możesz przypisać swoją wewnętrzną funkcję do zmiennej w zewnętrznym zasięgu lub przekazać ją jako argument do innej funkcji, w której można ją wywołać natychmiast lub w dowolnym momencie później. W związku z tym prawdopodobnie powstaje zamknięcie funkcji otaczającej zaraz po wywołaniu funkcji zamykającej ponieważ każda funkcja wewnętrzna ma dostęp do tego zamknięcia za każdym razem, gdy wywoływana jest funkcja wewnętrzna, przed lub po powrocie funkcji zamykającej.
  • Zamknięcie nie odnosi się do kopii pliku stare wartości zmiennych w swoim zakresie. Same zmienne są częścią zamknięcia, a więc wartość widoczna podczas uzyskiwania dostępu do jednej z tych zmiennych jest najnowszą wartością w momencie, w którym jest ona dostępna. Dlatego wewnętrzne funkcje tworzone w pętlach mogą być trudne, ponieważ każdy z nich ma dostęp do tych samych zmiennych zewnętrznych, zamiast chwytania kopii zmiennych w czasie tworzenia lub wywoływania funkcji.
  • "Zmienne" w zamknięciu obejmują wszelkie nazwane funkcje zadeklarowane w funkcji. Obejmują one również argumenty funkcji. Zamknięcie ma również dostęp do zmiennych zawierających jego zamknięcie, aż do zasięgu globalnego.
  • Zamknięcia wykorzystują pamięć, ale nie powodują wycieków pamięci ponieważ JavaScript sam oczyszcza własne struktury kołowe, które nie są przywoływane. Przecieki pamięci programu Internet Explorer związane z zamknięciami są tworzone, gdy nie można odłączyć wartości atrybutów DOM, które odwołują się do zamknięć, dzięki czemu zachowywane są odwołania do możliwych struktur kołowych.

343



Przy okazji dodałem tę "odpowiedź" z objaśnieniami, aby bezpośrednio nie odnosić się do pierwotnego pytania. Zamiast tego mam nadzieję, że jakakolwiek prosta odpowiedź (dla sześciolatka) nie wprowadza błędnych wyobrażeń na ten złożony temat. Na przykład. popularna wiki-odpowiedź powyżej mówi: "Zamknięcie jest, gdy zwrócisz wewnętrzną funkcję." Pomijając błędy gramatyczne, jest to technicznie błędne. - dlaliberte
James, powiedziałem, że zamknięcie jest "prawdopodobnie" stworzone w momencie wywołania funkcji zamykającej, ponieważ jest prawdopodobne, że implementacja może odroczyć utworzenie zamknięcia do pewnego czasu, kiedy zdecyduje, że zamknięcie jest absolutnie potrzebne. Jeśli nie ma funkcji wewnętrznej zdefiniowanej w funkcji zamykającej, wówczas żadne zamknięcie nie będzie potrzebne. Może więc może poczekać, aż zostanie utworzona pierwsza funkcja wewnętrzna, aby następnie utworzyć zamknięcie z kontekstu wywołania funkcji otaczającej. - dlaliberte
@ Beetroot-Beetroot Załóżmy, że mamy wewnętrzną funkcję, która jest przekazywana do innej funkcji, w której jest używana przed funkcja zewnętrzna powraca i przypuszczamy, że zwracamy również tę samą funkcję wewnętrzną z funkcji zewnętrznej. Jest to identyczna ta sama funkcja w obu przypadkach, ale mówisz, że zanim funkcja zewnętrzna powróci, wewnętrzna funkcja jest "związana" do stosu wywołań, podczas gdy po jej powrocie funkcja wewnętrzna jest nagle związana z zamknięciem. Zachowuje się identycznie w obu przypadkach; semantyka jest identyczna, więc czy nie mówisz tylko o szczegółach implementacji? - dlaliberte
@ Beetroot-Beetroot, dziękuję za twoją opinię i cieszę się, że cię zmusiłem. Nadal nie widzę żadnej semantycznej różnicy między żywym kontekstem zewnętrznej funkcji a tym samym kontekstem, gdy staje się ona zamknięciem, gdy funkcja zwraca (jeśli rozumiem twoją definicję). Wewnętrzna funkcja nie ma znaczenia. Wyrzucanie śmieci nie ma znaczenia, ponieważ funkcja wewnętrzna zachowuje odwołanie do kontekstu / zamknięcia w obu kierunkach, a wywołująca funkcja zewnętrzna po prostu odrzuca odwołanie do kontekstu wywołania. Ale jest to mylące dla ludzi i może lepiej nazwać to kontekstem połączenia. - dlaliberte
Ten artykuł jest trudny do odczytania, ale myślę, że faktycznie wspiera to, co mówię. Mówi: "Zamknięcie tworzy się przez zwrócenie obiektu funkcji [...] lub bezpośrednie przypisanie odniesienia do takiego obiektu funkcji do, na przykład, zmiennej globalnej." Nie mam na myśli, że GC nie ma znaczenia. Raczej ze względu na GC, a ponieważ wewnętrzna funkcja jest dołączona do kontekstu wywołania funkcji zewnętrznej (lub [[scope]], jak mówi artykuł), to nie ma znaczenia, czy wywołanie funkcji zewnętrznej powraca, ponieważ to powiązanie z wewnętrzną funkcja jest ważną rzeczą. - dlaliberte


OK, 6-letni wentylator. Czy chcesz usłyszeć najprostszy przykład zamknięcia?

Wyobraźmy sobie następną sytuację: kierowca siedzi w samochodzie. Ten samochód jest w samolocie. Samolot jest na lotnisku. Zdolność kierowcy do dostępu do rzeczy poza jego samochodem, ale wewnątrz samolotu, nawet jeśli ten samolot opuszcza lotnisko, jest zamknięciem. to jest to! Kiedy skończysz 27 lat, spójrz na bardziej szczegółowe wyjaśnienie lub na poniższym przykładzie.

Oto, w jaki sposób mogę zamienić historię mojego samolotu na kod.

var plane = function (defaultAirport) {

    var lastAirportLeft = defaultAirport;

    var car = {
        driver: {
            startAccessPlaneInfo: function () {
                setInterval(function () {
                    console.log("Last airport was " + lastAirportLeft);
                }, 2000);
            }
        }
    };
    car.driver.startAccessPlaneInfo();

    return {
        leaveTheAirport: function (airPortName) {
            lastAirportLeft = airPortName;
        }
    }
}("Boryspil International Airport");

plane.leaveTheAirport("John F. Kennedy");

331



Dobrze się gra i odpowiada na oryginalny plakat. Myślę, że to najlepsza odpowiedź. Zamierzałem użyć bagażu w podobny sposób: wyobraź sobie, że chodzisz do domu babci i pakujesz swoją skrzynię nintendo DS z kartami do gry w skrzynce, ale potem pakujesz skrzynkę do plecaka i wkładasz karty do gier w kieszeniach plecaka, Następnie umieściłeś całość w dużej walizce z większą ilością kart do gry w kieszeniach walizki. Gdy dotrzesz do domu babci, możesz grać w dowolną grę na swoim koncie DS, o ile wszystkie zewnętrzne sprawy są otwarte. lub coś takiego. - slartibartfast


ZA zamknięcie jest bardzo podobny do obiektu. Tworzy się instancję za każdym razem, gdy wywołujesz funkcję.

Zakres a zamknięcie w JavaScript jest leksykalny, co oznacza, że ​​wszystko, co zawarte jest w funkcji zamknięcie należy do, ma dostęp do każdej zmiennej, która jest w nim.

Zmienna jest zawarta w zamknięcie Jeśli ty

  1. przypisz go przy pomocy var foo=1; lub
  2. tylko napisz var foo;

Jeśli funkcja wewnętrzna (funkcja zawarta w innej funkcji) uzyskuje dostęp do takiej zmiennej, nie definiując jej we własnym zakresie za pomocą var, modyfikuje ona zawartość zmiennej w zewnętrznym zamknięcie.

ZA zamknięcie przeżywa czas działania funkcji, która ją zainicjowała. Jeśli inne funkcje usuwają to z zamknięcie / zakresw którym są zdefiniowane (na przykład jako wartości zwracane), będą one nadal się do tego odwoływać zamknięcie.

Przykład

 function example(closure) {
   // define somevariable to live in the closure of example
   var somevariable = 'unchanged';

   return {
     change_to: function(value) {
       somevariable = value;
     },
     log: function(value) {
       console.log('somevariable of closure %s is: %s',
         closure, somevariable);
     }
   }
 }

 closure_one = example('one');
 closure_two = example('two');

 closure_one.log();
 closure_two.log();
 closure_one.change_to('some new value');
 closure_one.log();
 closure_two.log();

Wydajność

somevariable of closure one is: unchanged
somevariable of closure two is: unchanged
somevariable of closure one is: some new value
somevariable of closure two is: unchanged

320



Wow, nigdy nie wiedziałem, że możesz użyć substytucji ciągów w console.log tak. Jeśli ktoś jest zainteresowany, jest więcej: developer.mozilla.org/en-US/docs/DOM/... - Flash
Zmienne, które znajdują się na liście parametrów funkcji, są również częścią zamknięcia (na przykład nie tylko ograniczone do var). - Thomas Eding