Jak działają zamknięcia JavaScript?


7636

Jak wyjaśniłbyś zamknięcia JavaScript komuś, kto zna pojęcia, na które składają się (na przykład funkcje, zmienne i tym podobne), ale nie rozumie samych zamknięć?

Widziałem przykład programu podany na Wikipedii, ale niestety nie pomógł.


391
Mój problem z tymi i wieloma odpowiedziami polega na tym, że podchodzą do niego z abstrakcyjnej, teoretycznej perspektywy, a nie zaczynają od prostego wyjaśnienia, dlaczego konieczne są zamknięcia w Javascript i praktyczne sytuacje, w których ich używasz. Kończy się artykuł tl; dr, przez który trzeba się przez cały czas przerzucać, myśląc: „ale dlaczego?”. Chciałbym zacząć od: zamknięcia są dobrym sposobem na radzenie sobie z następującymi dwiema rzeczywistościami JavaScript: a. zakres jest na poziomie funkcji, a nie na poziomie bloku, i b. znaczna część tego, co robisz w JavaScript, jest asynchroniczna / sterowana zdarzeniami.
Jeremy Burton

53
@ Redsandro Po pierwsze, znacznie ułatwia pisanie kodu sterowanego zdarzeniami. Mogę uruchomić funkcję, gdy strona się ładuje, aby określić szczegóły dotyczące HTML lub dostępnych funkcji. Mogę zdefiniować i ustawić moduł obsługi w tej funkcji oraz mieć dostęp do wszystkich informacji kontekstowych za każdym razem, gdy moduł jest wywoływany, bez konieczności ponownego zapytania. Rozwiąż problem raz, użyj go ponownie na każdej stronie, na której ten moduł obsługi jest potrzebny, ze zmniejszonym narzutem związanym z ponownym wywołaniem modułu obsługi. Czy kiedykolwiek widziałeś, że te same dane są ponownie mapowane dwukrotnie w języku, który ich nie ma? Zamknięcia znacznie ułatwiają unikanie tego rodzaju rzeczy.
Erik Reppen

1
@Erik Reppen dzięki za odpowiedź. Właściwie byłem ciekawy korzyści płynących z tego trudnego do odczytania closurekodu, w przeciwieństwie do tego, Object Literalktóry ponownie wykorzystuje się sam i zmniejsza to samo obciążenie, ale wymaga o 100% mniej kodu owijania.
Redsandro

6
Dla programistów Java krótka odpowiedź jest taka, że ​​jest to funkcja równoważna klasie wewnętrznej. Klasa wewnętrzna zawiera także domyślny wskaźnik do wystąpienia klasy zewnętrznej i jest używana do tego samego celu (tj. Do tworzenia procedur obsługi zdarzeń).
Boris van Schooten,

8
Uważam, że ten praktyczny przykład jest bardzo przydatny: youtube.com/watch?v=w1s9PgtEoJs
Abhi

Odpowiedzi:


7357

Zamknięcie to połączenie:

  1. Funkcja i
  2. Odniesienie do zewnętrznego zakresu tej funkcji (środowisko leksykalne)

Środowisko leksykalne jest częścią każdego kontekstu wykonania (ramki stosu) i stanowi mapę między identyfikatorami (tj. Lokalnymi nazwami zmiennych) a wartościami.

Każda funkcja w JavaScript utrzymuje odniesienie do zewnętrznego środowiska leksykalnego. Odwołanie to służy do konfigurowania kontekstu wykonania utworzonego po wywołaniu funkcji. To odwołanie umożliwia kodowi wewnątrz funkcji „zobaczenie” zmiennych zadeklarowanych poza funkcją, niezależnie od tego, kiedy i gdzie funkcja jest wywoływana.

Jeśli funkcja została wywołana przez funkcję, która z kolei została wywołana przez inną funkcję, powstaje łańcuch odniesień do zewnętrznych środowisk leksykalnych. Ten łańcuch nazywa się łańcuchem zakresu.

W poniższym kodzie innertworzy zamknięcie ze środowiskiem leksykalnym kontekstu wykonania utworzonego po foowywołaniu, zamykając zmienną 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`

Innymi słowy: w JavaScript funkcje zawierają odniesienie do prywatnego „pola stanu”, do którego mają dostęp tylko one (i wszelkie inne funkcje zadeklarowane w tym samym środowisku leksykalnym). To pole stanu jest niewidoczne dla osoby wywołującej funkcję, zapewniając doskonały mechanizm ukrywania danych i enkapsulacji.

I pamiętaj: funkcje w JavaScript mogą być przekazywane jak zmienne (funkcje pierwszej klasy), co oznacza, że ​​te pary funkcjonalności i stanu mogą być przekazywane wokół twojego programu: podobnie jak możesz przekazać instancję klasy w C ++.

Gdyby JavaScript nie miał zamknięć, wówczas więcej funkcji musiałoby zostać jawnie przekazanych między funkcjami , dzięki czemu listy parametrów byłyby dłuższe, a kod zakłócany.

Jeśli więc chcesz, aby funkcja zawsze miała dostęp do prywatnego stanu, możesz użyć zamknięcia.

... i często my nie chcemy terytorium stowarzyszone z funkcji. Na przykład w Javie lub C ++, kiedy dodajesz zmienną instancji prywatnej i metodę do klasy, kojarzysz stan z funkcjonalnością.

W C i większości innych popularnych języków po zwróceniu funkcji wszystkie zmienne lokalne nie są już dostępne, ponieważ rama stosu jest zniszczona. W JavaScript, jeśli zadeklarujesz funkcję w innej funkcji, wówczas lokalne zmienne funkcji zewnętrznej mogą pozostać dostępne po powrocie z niej. W ten sposób, w powyższym kodzie secretpozostaje dostępny do obiektu funkcyjnego inner, po to został zwrócony z foo.

Zastosowania zamknięć

Zamknięcia są przydatne, gdy potrzebujesz prywatnego stanu powiązanego z funkcją. Jest to bardzo częsty scenariusz - i pamiętaj: JavaScript nie miał składni klas do 2015 r. I nadal nie ma składni pól prywatnych. Zamknięcia spełniają tę potrzebę.

Zmienne instancji prywatnej

W poniższym kodzie funkcja toStringzamyka szczegóły samochodu.

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())

Programowanie funkcjonalne

W poniższym kodzie funkcja innerzamyka się zarówno na, jak fni na 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

Programowanie zorientowane na zdarzenia

W poniższym kodzie funkcja onClickzamyka się nad zmienną 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>

Modularyzacja

W poniższym przykładzie wszystkie szczegóły implementacji są ukryte w natychmiast wykonywanym wyrażeniu funkcji. Funkcje ticki funkcje związane toStringz państwem prywatnym oraz funkcje potrzebne do ukończenia pracy. Zamknięcia umożliwiły nam modularyzację i enkapsulację naszego kodu.

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())

Przykłady

Przykład 1

Ten przykład pokazuje, że zmienne lokalne nie są kopiowane w zamknięciu: zamknięcie zachowuje odwołanie do samych zmiennych pierwotnych . To tak, jakby ramka stosu pozostała przy życiu w pamięci nawet po wyjściu z funkcji zewnętrznej.

function foo() {
  let x = 42
  let inner  = function() { console.log(x) }
  x = x+1
  return inner
}
var f = foo()
f() // logs 43

Przykład 2

W poniższym kodzie, trzech metod log, incrementa updatewszystko Zamknij nad samym środowisku leksykalnym.

I przy każdym createObjectwywołaniu tworzony jest nowy kontekst wykonania (ramka stosu) i tworzona jest zupełnie nowa zmienna xoraz nowy zestaw funkcji ( logitp.), Które zamykają tę nową zmienną.

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

Przykład 3

Jeśli używasz zmiennych zadeklarowanych za pomocą var, uważaj, aby zrozumieć, którą zmienną zamykasz. Zmienne zadeklarowane za pomocą varsą podnoszone. Jest to o wiele mniejszy problem we współczesnym JavaScript ze względu na wprowadzenie leti const.

W poniższym kodzie za każdym razem wokół pętli innertworzona jest nowa funkcja , która się zamyka i. Ponieważ jednak var ijest podnoszony poza pętlę, wszystkie te funkcje wewnętrzne zamykają się w tej samej zmiennej, co oznacza, że itrzykrotnie drukowana jest końcowa wartość (3).

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]() 
}

Punkty końcowe:

  • Ilekroć funkcja jest zadeklarowana w JavaScript, tworzone jest zamknięcie.
  • Zwracanie functionz wewnątrz innej funkcji jest klasycznym przykładem zamknięcia, ponieważ stan wewnątrz funkcji zewnętrznej jest domyślnie dostępny dla zwróconej funkcji wewnętrznej, nawet po zakończeniu wykonywania funkcji zewnętrznej.
  • Ilekroć używasz eval()wewnątrz funkcji, używane jest zamknięcie. Tekst, do którego evalmożesz odwoływać się do zmiennych lokalnych funkcji, aw trybie nieściągalnym możesz nawet tworzyć nowe zmienne lokalne za pomocą eval('var foo = …').
  • Kiedy używasz new Function(…)( konstruktora funkcji ) wewnątrz funkcji, nie zamyka się ona w swoim środowisku leksykalnym: zamyka się w kontekście globalnym. Nowa funkcja nie może odwoływać się do zmiennych lokalnych funkcji zewnętrznej.
  • Zamknięcie w JavaScript jest jak utrzymywanie odwołania ( a NIE kopii) do zakresu w punkcie deklaracji funkcji, który z kolei zachowuje odniesienie do jego zewnętrznego zakresu i tak dalej, aż do globalnego obiektu na górze łańcuch zasięgu.
  • Zamknięcie jest tworzone, gdy funkcja jest deklarowana; to zamknięcie służy do skonfigurowania kontekstu wykonania po wywołaniu funkcji.
  • Nowy zestaw zmiennych lokalnych jest tworzony przy każdym wywołaniu funkcji.

Spinki do mankietów


74
Brzmi nieźle: „Zamknięcie w JavaScript jest jak przechowywanie kopii wszystkich lokalnych zmiennych, tak jak były po wyjściu funkcji”. Jest to jednak mylące z kilku powodów. (1) Wywołanie funkcji nie musi wychodzić, aby utworzyć zamknięcie. (2) Nie jest to kopia wartości zmiennych lokalnych, ale same zmienne. (3) Nie mówi, kto ma dostęp do tych zmiennych.
dlaliberte

27
Przykład 5 pokazuje „gotcha”, w którym kod nie działa zgodnie z przeznaczeniem. Ale nie pokazuje, jak to naprawić. Ta druga odpowiedź pokazuje sposób, aby to zrobić.
Matt

190
Podoba mi się, jak ten post zaczyna się dużymi pogrubionymi literami, mówiąc: „Zamknięcia nie są magiczne”, a kończy swój pierwszy przykład „Magią jest to, że w JavaScript odwołanie do funkcji ma również tajne odniesienie do zamknięcia, w którym zostało utworzone”.
Andrew Macheret,

6
Przykład 3 to mieszanie zamknięć z podnoszeniem skryptów javascript. Teraz myślę, że wyjaśnienie tylko zamknięć jest wystarczająco trudne bez wprowadzenia zachowania podnoszenia. To pomogło mi najbardziej: Closures are functions that refer to independent (free) variables. In other words, the function defined in the closure 'remembers' the environment in which it was created.from developer.mozilla.org/en-US/docs/Web/JavaScript/Closures
caramba

3
ECMAScript 6 może coś zmienić w tym świetnym artykule na temat zamknięcia. Na przykład, jeśli użyjesz let i = 0zamiast var i = 0w przykładzie 5, to testList()wydrukuje to, co chcesz oryginalnie.
Nier

3988

Każda funkcja w JavaScript utrzymuje link do zewnętrznego środowiska leksykalnego. Środowisko leksykalne to mapa wszystkich nazw (np. Zmiennych, parametrów) w zakresie wraz z ich wartościami.

Tak więc, ilekroć zobaczysz functionsłowo kluczowe, kod wewnątrz tej funkcji ma dostęp do zmiennych zadeklarowanych poza funkcją.

function foo(x) {
  var tmp = 3;

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

  bar(10);
}

foo(2);

To się zaloguje, 16ponieważ funkcja barzamyka parametr xi zmienną tmp, które istnieją w środowisku leksykalnym funkcji zewnętrznej foo.

Funkcja barwraz z jej powiązaniem ze środowiskiem leksykalnym funkcji foojest zakończeniem.

Funkcja nie musi zwracać się , aby utworzyć zamknięcie. Po prostu ze względu na swoją deklarację każda funkcja zamyka się w otaczającym ją środowisku leksykalnym, tworząc 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(10); // 16
bar(10); // 17

Powyższa funkcja rejestruje również 16, ponieważ kod wewnątrz barmoże nadal odwoływać się do argumentów xi zmiennych tmp, nawet jeśli nie są one już bezpośrednio objęte zakresem.

Ponieważ jednak tmpnadal kręci się wewnątrz barzamknięcia, można go zwiększać. Będzie zwiększany za każdym razem, gdy zadzwonisz bar.

Najprostszym przykładem zamknięcia jest:

var a = 10;

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

Po wywołaniu funkcji JavaScript ectworzony jest nowy kontekst wykonania . Wraz z argumentami funkcji i obiektem docelowym ten kontekst wykonania otrzymuje również łącze do środowiska leksykalnego wywołującego kontekstu wykonania, co oznacza, że ​​zmienne zadeklarowane w zewnętrznym środowisku leksykalnym (w powyższym przykładzie zarówno ai b) są dostępne z ec.

Każda funkcja tworzy zamknięcie, ponieważ każda funkcja ma link do zewnętrznego środowiska leksykalnego.

Zauważ, że same zmienne są widoczne z wnętrza zamknięcia, a nie z kopii.


24
@feeela: Tak, każda funkcja JS tworzy zamknięcie. Zmienne, do których się nie odwołujemy, prawdopodobnie zostaną zakwalifikowane do wyrzucania elementów bezużytecznych w nowoczesnych silnikach JS, ale nie zmienia to faktu, że podczas tworzenia kontekstu wykonania kontekst ten zawiera odniesienie do otaczającego kontekstu wykonania i jego zmiennych oraz ta funkcja jest obiektem, który może zostać przeniesiony do innego zakresu zmiennej, zachowując to oryginalne odniesienie. To jest zamknięcie.

@Ali Właśnie odkryłem, że jsFiddle, który podałem, tak naprawdę niczego nie dowodzi, ponieważ deletezawodzi. Niemniej jednak środowisko leksykalne, które funkcja będzie nosić jako [[Zakres]] (i ostatecznie użyje jako podstawy dla własnego środowiska leksykalnego po wywołaniu) jest określane, gdy zostanie wykonana instrukcja definiująca funkcję. Oznacza to, że funkcja jest zamykanie nad całą zawartością w zakresie wykonania, niezależnie od tego, która wartość jest w rzeczywistości odnosi się do tego, czy ucieka zakresu. Proszę spojrzeć na sekcje 13.2 i 10 w specyfikacji
Asad Saeeduddin,

8
To była dobra odpowiedź, dopóki nie spróbowała wyjaśnić prymitywnych typów i odniesień. Robi się to zupełnie nie tak i mówi o kopiowaniu literałów, co tak naprawdę nie ma z tym nic wspólnego.
Ry-

12
Zamknięcia są odpowiedzią JavaScript na programowanie obiektowe oparte na klasach. JS nie jest oparty na klasach, więc trzeba było znaleźć inny sposób na implementację niektórych rzeczy, których inaczej nie można by zaimplementować.
Bartłomiej Zalewski

2
to powinna być zaakceptowana odpowiedź. Magia nigdy nie dzieje się w funkcji wewnętrznej. Dzieje się tak, gdy przypisujemy funkcję zewnętrzną do zmiennej. Tworzy to nowy kontekst wykonania dla funkcji wewnętrznej, dzięki czemu można gromadzić „zmienną prywatną”. Oczywiście może, ponieważ zmienna przypisana do funkcji zewnętrznej zachowała kontekst. Pierwsza odpowiedź tylko komplikuje sprawę bez wyjaśniania, co się tam naprawdę dzieje.
Albert Gao,

2442

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

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

Czy ktoś może uznać, że mam 6 lat i jestem dziwnie zainteresowany tym tematem?

Jestem pewien, że byłem jedną z niewielu osób, które próbowały dosłownie odpowiedzieć na pierwsze pytanie. Od tego czasu pytanie kilkakrotnie mutowało, więc moja odpowiedź może teraz wydawać się niesamowicie głupia i nie na miejscu. Mamy nadzieję, że ogólny pomysł tej historii jest dla niektórych zabawny.


Jestem wielkim fanem analogii i metafory podczas wyjaśniania trudnych pojęć, więc pozwól mi spróbować swoich sił z historią.

Pewnego razu:

Była księżniczka ...

function princess() {

Mieszkała w cudownym świecie pełnym przygód. Poznała swojego księcia z bajki, jeździła po świecie jednorożcem, walczyła ze smokami, napotykała gadające zwierzęta i wiele innych fantastycznych rzeczy.

    var adventures = [];

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

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

    /* ... */

Ale zawsze musiała wracać do nudnego świata obowiązków i dorosłych.

    return {

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

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

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

var littleGirl = princess();

... opowiadanie historii o magii i fantastyce.

littleGirl.story();

I chociaż dorośli wiedzieli o prawdziwych księżniczkach, nigdy nie uwierzyliby w jednorożce lub smoki, ponieważ nigdy ich nie zobaczą. Dorośli powiedzieli, że istnieją tylko w wyobraźni małej dziewczynki.

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

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


340
Naprawdę uwielbiam to wyjaśnienie. Dla tych, którzy ją czytają i nie przestrzegają, analogia jest następująca: funkcja princess () jest złożonym zakresem zawierającym prywatne dane. Poza funkcją danych prywatnych nie można zobaczyć ani uzyskać do nich dostępu. Księżniczka trzyma w swojej wyobraźni jednorożce, smoki, przygody itp., A dorośli nie mogą ich zobaczyć. ALE wyobraźnia księżniczki jest uwięziona w zamknięciu story()funkcji, która jest jedynym interfejsem, jaki littleGirlinstancja udostępnia w świecie magii.
Patrick M,

Więc oto storyzamknięcie, ale gdyby kod był, var story = function() {}; return story;to littleGirlbyłoby zamknięcie. Przynajmniej takie wrażenie wywarło na mnie zastosowanie przez MDN „prywatnych” metod z zamknięciami : „Te trzy funkcje publiczne to zamknięcia, które dzielą to samo środowisko”.
icc97

16
@ icc97, tak, storyjest zamknięciem odnoszącym się do środowiska przewidzianego w zakresie princess. princessjest także innym domniemanym zamknięciem, tj. princessi littleGirlpodzieliłyby wszelkie odniesienia do parentstablicy, która istniałaby z powrotem w środowisku / zakresie, w którym littleGirlistnieje i princessjest zdefiniowana.
Jacob Swartwood

6
@BenjaminKrupp Dodałem wyraźny komentarz do kodu, aby pokazać / zasugerować, że w treści jest więcej operacji princessniż to, co napisano. Niestety, ta historia jest teraz trochę nie na miejscu w tym wątku. Pierwotnie pytanie dotyczyło „wyjaśnienia zamknięć JavaScript dla 5-latka”; 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 5-latka.
Jacob Swartwood

11
W rzeczywistości dla mnie to miało sens. I muszę przyznać, że wreszcie zrozumienie zamknięcia JS za pomocą opowieści o księżniczkach i przygodach sprawia, że ​​czuję się trochę dziwnie.
Krystalizacja

753

Biorąc to pytanie na poważnie, powinniśmy dowiedzieć się, co typowy 6-latek potrafi poznawczo, choć wprawdzie osoba zainteresowana JavaScriptem nie jest taka typowa.

O rozwoju dzieciństwa: od 5 do 7 lat mówi:

Twoje dziecko będzie mogło wykonać dwustopniowe instrukcje. Na przykład, jeśli powiesz swojemu dziecku: „Idź do kuchni i przynieś mi kosz na śmieci”, będą mogli zapamiętać ten kierunek.

Możemy użyć tego przykładu, aby wyjaśnić zamknięcia w następujący sposób:

Kuchnia to zamknięcie, które ma lokalną zmienną o nazwie trashBags. W kuchni jest funkcja o nazwie, getTrashBagktóra dostaje jeden worek na śmieci i zwraca go.

Możemy to zakodować w JavaScript w następujący sposób:

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

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

  • Za każdym razem, gdy makeKitchen()jest wywoływane, tworzone jest nowe zamknięcie z osobnym trashBags.
  • trashBagsZmienna jest lokalny do wnętrza każdej kuchni i nie jest dostępny na zewnątrz, ale wewnętrzna funkcja na getTrashBagwłasność ma do niego dostępu.
  • Każde wywołanie funkcji tworzy zamknięcie, ale nie byłoby potrzeby utrzymywania zamknięcia, chyba że funkcja wewnętrzna, która ma dostęp do wnętrza zamknięcia, może zostać wywołana z zewnątrz zamknięcia. Zwraca obiekt za pomocą tej getTrashBagfunkcji.

6
W rzeczywistości, myląco, wywołanie funkcji makeKitchen jest faktycznym zamknięciem, a nie zwróconym obiektem kuchennym.
dlaliberte

6
Przechodząc przez inne, znalazłem tę odpowiedź jako najłatwiejszy sposób na wyjaśnienie, co i dlaczego zamknięcie. Jest.
Chetabahana,

3
Za dużo menu i przystawek, za mało mięsa i ziemniaków. Możesz poprawić tę odpowiedź za pomocą jednego krótkiego zdania, takiego jak: „Zamknięcie jest zapieczętowanym kontekstem funkcji z powodu braku jakiegokolwiek mechanizmu określania zakresu zapewnianego przez klasy”.
Staplerfahrer

584

The Straw Man

Muszę wiedzieć, ile razy kliknięto przycisk i robić co trzecie kliknięcie ...

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 zakres zewnętrzny, dodając zmienną, której jedynym celem jest śledzenie liczby. W niektórych sytuacjach byłoby to preferowane, ponieważ zewnętrzna aplikacja może potrzebować dostępu do tych informacji. Ale w tym przypadku zmieniamy zachowanie tylko co trzecie kliknięcie, dlatego lepiej jest zawrzeć tę funkcjonalność w module obsługi zdarzeń .

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 JavaScript w zamykaniu. To zachowanie umożliwia dowolnej funkcji dostęp do zakresu, w którym została utworzona, w nieskończoność. Aby praktycznie to zastosować, natychmiast wywołuję funkcję, która zwraca inną funkcję, a ponieważ funkcja, którą zwracam, ma dostęp do wewnętrznej zmiennej zliczania (z powodu wyjaśnionego powyżej zachowania dotyczącego zamykania), skutkuje to zakresem prywatnym do użycia przez wynikowy funkcja ... Nie takie proste? Rozcieńczmy to ...

Proste zamknięcie w 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 zwracaną funkcją są dostępne dla zwróconej funkcji, ale nie są bezpośrednio dostępne dla zwróconego obiektu funkcji ...

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

Zdobyć? Tak więc w naszym podstawowym przykładzie zmienna count jest zawarta w zamknięciu i zawsze dostępna dla procedury obsługi zdarzeń, więc zachowuje swój stan od kliknięcia do kliknięcia.

Ponadto, ten prywatny stan zmiennych jest w pełni dostępny, zarówno dla odczytów, jak i przypisywania do prywatnych zmiennych o zasięgu.

Proszę bardzo; teraz całkowicie ujmujesz to zachowanie.

Pełny post na blogu (w tym uwagi dotyczące jQuery)


11
Nie zgadzam się z twoją definicją tego, czym jest zamknięcie. Nie ma powodu, by musiał się sam nazywać. Jest również nieco uproszczone (i niedokładne) stwierdzenie, że należy go „zwrócić” (dużo dyskusji na ten temat w komentarzach do głównej odpowiedzi na to pytanie)
James Montagne

40
@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 dla mnie stare i rozwiązane, całkowicie zasługuje na +1.
e-satis

84
„Muszę wiedzieć, ile razy kliknięto przycisk, i robić co trzecie kliknięcie ...” To zwróciło moją uwagę. Przypadek użycia i rozwiązanie pokazujące, że zamknięcie nie jest tak tajemniczą rzeczą i że wiele z nas je pisało, ale nie znało 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 kliknięty zostanie „element”. Bardzo informujące!
Adam

+1 za zachowanie zamknięcia . Czy możemy ograniczyć zachowanie zamknięcia do funkcji w javascript lub tę koncepcję można zastosować również do innych struktur języka?
Dziamid,

492

Zamknięcia są trudne do wyjaśnienia, ponieważ są wykorzystywane do działania pewnych zachowań, których i tak wszyscy intuicyjnie oczekują. Znajduję najlepszy sposób na ich wyjaśnienie (i sposób, w jaki dowiedziałem się, co robią) to wyobrażenie sobie sytuacji bez nich:

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

Co by się tu stało, gdyby JavaScript nie wiedział o zamknięciach? Po prostu zamień wywołanie w ostatnim wierszu na treść metody (czyli w zasadzie to, co robią wywołania funkcji), a otrzymasz:

console.log(x + 3);

Gdzie jest definicja x? Nie zdefiniowaliśmy tego w bieżącym zakresie. Jedynym rozwiązaniem jest plus5 przeniesienie jego zakresu (a raczej zakresu jego rodzica). W ten sposób xjest dobrze zdefiniowane i powiązane z wartością 5.


11
Jest to dokładnie taki przykład, który wprowadza wielu ludzi w błąd, myśląc, że to wartości są używane w zwracanej funkcji, a nie sama zmienna zmienna. Gdyby zmieniono ją na „return x + = y”, lub jeszcze lepiej zarówno tę, jak i inną funkcję „x * = y”, byłoby jasne, że nic nie jest kopiowane. W przypadku osób stosujących ramki w stosie wyobraź sobie, że zamiast nich stosujesz ramki sterty, które mogą istnieć po powrocie funkcji.
Matt

14
@Matt Nie zgadzam się. Przykład nie powinien wyczerpująco dokumentować wszystkich właściwości. Ma on być redukcyjny i zilustrować istotną cechę koncepcji. OP poprosił o proste wyjaśnienie („dla sześciolatka”). Przyjmij przyjętą odpowiedź: zupełnie nie daje ona zwięzłego wyjaśnienia, właśnie dlatego, że stara się być wyczerpująca. (Zgadzam się z tobą, że ważną właściwością JavaScript jest to, że wiązanie jest raczej odniesieniem niż wartością… ale znowu skuteczne wyjaśnienie to takie, które sprowadza się do absolutnego minimum.)
Konrad Rudolph,

@KonradRudolph Podoba mi się styl i zwięzłość twojego przykładu. Po prostu zalecam nieznaczną zmianę, aby ostatnia część „Jedynym rozwiązaniem jest ...” stała się prawdą. Obecnie nie jest w rzeczywistości inny, prostsze rozwiązanie do scenariusza, który ma nie odpowiadają kontynuacje JavaScript, a nie odpowiadają wspólnym nieporozumieniem o co kontynuacje są. Tak więc przykład w obecnej formie jest niebezpieczny. Nie ma to nic wspólnego z wyczerpującym zestawieniem właściwości, lecz ze zrozumieniem, czym jest x w zwróconej funkcji, co jest przecież głównym punktem.
Matt

@Matt Hmm, nie jestem pewien, czy cię w pełni rozumiem, ale zaczynam rozumieć, że możesz mieć rację. Ponieważ komentarze są zbyt krótkie, czy możesz wyjaśnić, co masz na myśli w gist / pastie lub na czacie? Dzięki.
Konrad Rudolph

2
@KonradRudolph Wydaje mi się, że nie wiedziałem jasno o celu x + = y. Chodziło o to, aby pokazać, że wielokrotne wywołania zwracanej funkcji wciąż używają tej samej zmiennej x (w przeciwieństwie do tej samej wartości , którą ludzie wyobrażają sobie, że jest „wstawiana” podczas tworzenia funkcji). To jak dwa pierwsze alerty w skrzypcach. Celem dodatkowej funkcji x * = y byłoby pokazanie, że wiele zwracanych funkcji ma ten sam x.
Matt

376

OK, 6-letni wentylator do zamykania. 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 uzyskania dostępu do rzeczy poza samochodem, ale w samolocie, nawet jeśli samolot ten opuszcza lotnisko, jest zamknięciem. Otóż ​​to. Kiedy skończysz 27 lat, spójrz na bardziej szczegółowe wyjaśnienie lub na poniższy przykład.

Oto jak mogę przekonwertować 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");


26
Dobrze zagrany i odpowiada na oryginalny plakat. Myślę, że to najlepsza odpowiedź. Zamierzałem używać bagażu w podobny sposób: wyobraź sobie, że idziesz do domu babci, a ty pakujesz swoją skrzynkę Nintendo DS z kartami do gry, ale potem pakujesz ją do plecaka, a także wkładasz karty do kieszeni i Następnie włóż całość do dużej walizki z większą liczbą kart do gry w kieszeniach walizki. Gdy dotrzesz do domu babci, możesz grać w dowolną grę na swoim DS, o ile wszystkie zewnętrzne skrzynki są otwarte. lub coś w tym rodzaju.
slartibartfast

376

TLDR

Zamknięcie jest łącznikiem między funkcją a jej zewnętrznym środowiskiem leksykalnym (tj. W formie pisemnej), dzięki czemu identyfikatory (zmienne, parametry, deklaracje funkcji itp.) Zdefiniowane w tym środowisku są widoczne z wnętrza funkcji, niezależnie od tego, kiedy lub z gdzie wywoływana jest funkcja.

Detale

W terminologii specyfikacji ECMAScript można powiedzieć, że zamknięcie jest realizowane przez [[Environment]]odwołanie do każdego obiektu funkcji, co wskazuje na środowisko leksykalne, w którym funkcja jest zdefiniowana.

Gdy funkcja jest wywoływana za pomocą [[Call]]metody wewnętrznej , [[Environment]]odwołanie do obiektu funkcji jest kopiowane do zewnętrznego odwołania do środowiska rekordu środowiska nowo utworzonego kontekstu wykonania (ramka stosu).

W poniższym przykładzie funkcja fzamyka się w środowisku leksykalnym globalnego kontekstu wykonania:

function f() {}

W poniższym przykładzie funkcja hzamyka środowisko leksykalne funkcji g, które z kolei zamyka środowisko leksykalne globalnego kontekstu wykonania.

function g() {
    function h() {}
}

Jeśli funkcja wewnętrzna zostanie zwrócona przez funkcję zewnętrzną, wówczas zewnętrzne środowisko leksykalne pozostanie po powrocie funkcji zewnętrznej. Wynika to z faktu, że zewnętrzne środowisko leksykalne musi być dostępne, jeśli funkcja wewnętrzna zostanie ostatecznie wywołana.

W poniższym przykładzie funkcja jzamyka się w środowisku leksykalnym funkcji i, co oznacza, że ​​zmienna xjest widoczna z wnętrza funkcji j, długo po izakończeniu wykonywania funkcji:

function i() {
    var x = 'mochacchino'
    return function j() {
        console.log('Printing the value of x, from within function j: ', x)
    }
} 

const k = i()
setTimeout(k, 500) // invoke k (which is j) after 500ms

W zamknięciu, zmienne w zewnętrznym środowisku leksykalnym sami są dostępne, nie kopie.

function l() {
  var y = 'vanilla';

  return {
    setY: function(value) {
      y = value;
    },
    logY: function(value) {
      console.log('The value of y is: ', y);
    }
  }
}

const o = l()
o.logY() // The value of y is: vanilla
o.setY('chocolate')
o.logY() // The value of y is: chocolate

Łańcuch środowisk leksykalnych, połączony między kontekstami wykonywania za pomocą zewnętrznych odniesień do środowiska, tworzy łańcuch zasięgu i definiuje identyfikatory widoczne z dowolnej funkcji.

Należy pamiętać, że próbując poprawić jasność i dokładność, odpowiedź została znacznie zmieniona w stosunku do oryginału.


56
Wow, nigdy nie wiedziałem, że możesz użyć w console.logtym celu podstawień łańcuchów . Jeśli ktoś jest zainteresowany, jest ich więcej: developer.mozilla.org/en-US/docs/DOM/…
Flash

7
Zmienne znajdujące się na liście parametrów funkcji są również częścią zamknięcia (np. Nie tylko var).
Thomas Eding,

Zamknięcia brzmią bardziej jak przedmioty i klasy itp. Nie jestem pewien, dlaczego wiele osób nie porównuje tych dwóch - łatwiej byłoby nam, początkującym, nauczyć się!
almaruf

365

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

  • Zamknięcie jest tworzone nie tylko po zwróceniu funkcji wewnętrznej. W rzeczywistości funkcja zamykająca wcale nie musi powracać, aby utworzyć jej zamknięcie. Zamiast tego możesz przypisać swoją funkcję wewnętrzną do zmiennej w zakresie zewnętrznym lub przekazać ją jako argument innej funkcji, w której można ją wywołać natychmiast lub w dowolnym momencie później. Dlatego zamknięcie funkcji zamykającej jest prawdopodobnie tworzone natychmiast 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 odwołuje się do kopii starych wartości zmiennych w swoim zakresie. Same zmienne są częścią zamknięcia, więc wartość widoczna podczas uzyskiwania dostępu do jednej z tych zmiennych jest najnowszą wartością w momencie dostępu. Dlatego funkcje wewnętrzne utworzone wewnątrz pętli mogą być trudne, ponieważ każda z nich ma dostęp do tych samych zmiennych zewnętrznych zamiast pobierać kopię zmiennych w momencie tworzenia lub wywoływania funkcji.
  • „Zmienne” w zamknięciu obejmują wszelkie nazwane funkcje zadeklarowane w funkcji. Zawierają również argumenty funkcji. Zamknięcie ma również dostęp do zmiennych zawierających zamknięcie, aż do globalnego zasięgu.
  • Zamknięcia używają pamięci, ale nie powodują wycieków pamięci, ponieważ JavaScript sam w sobie czyści własne struktury kołowe, do których nie ma odniesienia. Wycieki pamięci w programie Internet Explorer dotyczące zamknięć są tworzone, gdy nie rozłącza wartości atrybutów DOM odwołujących się do zamknięć, utrzymując w ten sposób odniesienia do ewentualnie okrągłych struktur.

15
James, powiedziałem, że zamknięcie jest „prawdopodobnie” tworzone w momencie wywołania funkcji zamykającej, ponieważ jest prawdopodobne, że implementacja może odroczyć utworzenie zamknięcia do czasu, kiedy uzna, że ​​zamknięcie jest absolutnie potrzebne. Jeśli w funkcji zamykającej nie zdefiniowano żadnej funkcji wewnętrznej, zamknięcie nie będzie konieczne. Może więc może poczekać, aż pierwsza funkcja wewnętrzna zostanie utworzona, aby następnie utworzyć zamknięcie z kontekstu wywołania funkcji zamykającej.
dlaliberte

9
@ Beetroot-Beetroot Załóżmy, że mamy funkcję wewnętrzną, która jest przekazywana do innej funkcji, w której jest używana, zanim funkcja zewnętrzna powróci, i załóżmy, że zwracamy tę samą funkcję wewnętrzną z funkcji zewnętrznej. Jest to identyczna funkcja w obu przypadkach, ale mówisz, że zanim funkcja zewnętrzna powróci, funkcja wewnętrzna jest „powiązana” ze stosem wywołań, podczas gdy po powrocie funkcja wewnętrzna jest nagle związana z zamknięciem. W obu przypadkach zachowuje się identycznie; semantyka jest identyczna, więc nie mówisz tylko o szczegółach implementacji?
dlaliberte

7
@ Beetroot-Beetroot, dziękuję za opinie i cieszę się, że udało mi się pomyśleć. Nadal nie widzę żadnej semantycznej różnicy między kontekstem na żywo funkcji zewnętrznej a tym samym kontekstem, gdy staje się ona zamknięta w momencie powrotu funkcji (jeśli rozumiem twoją definicję). Funkcja wewnętrzna nie ma znaczenia. Odśmiecanie nie ma znaczenia, ponieważ funkcja wewnętrzna zachowuje odniesienie do kontekstu / zamknięcia w obu kierunkach, a wywołujący funkcję zewnętrzną po prostu upuszcza swoje odniesienie do kontekstu wywołania. Jest to jednak mylące dla ludzi i być może lepiej nazwać to kontekstem połączenia.
dlaliberte

9
Ten artykuł jest trudny do odczytania, ale wydaje mi się, że faktycznie popiera to, co mówię. Mówi: „Zamknięcie jest tworzone przez zwrócenie obiektu funkcji [...] lub przez bezpośrednie przypisanie odwołania do takiego obiektu funkcji na przykład do zmiennej globalnej”. Nie chodzi mi o to, że GC nie ma znaczenia. Raczej z powodu GC i ponieważ funkcja wewnętrzna jest dołączona do kontekstu wywołania funkcji zewnętrznej (lub [[zakres]], jak mówi artykuł), to nie ma znaczenia, czy wywołanie funkcji zewnętrznej powraca, ponieważ to wiązanie z wewnętrzną funkcja jest ważna.
dlaliberte

3
Świetna odpowiedź! Jedną rzeczą, którą należy dodać, jest to, że wszystkie funkcje zamykają się w całej zawartości zakresu wykonawczego, w którym są zdefiniowane. Nie ma znaczenia, czy odnoszą się one do niektórych zmiennych, czy do żadnej ze zmiennych nadrzędnych: odniesienie do środowiska leksykalnego zakresu nadrzędnego jest bezwarunkowo przechowywane jako [[Zakres]]. Można to zobaczyć w części poświęconej tworzeniu funkcji w specyfikacji ECMA.
Asad Saeeduddin,

236

Niedawno napisałem post na blogu wyjaśniający zamknięcia. Oto, co powiedziałem o zamknięciach pod względem tego, dlaczego chcesz je mieć.

Zamknięcia są sposobem na to, aby funkcja miała trwałe, prywatne zmienne - to znaczy zmienne, o których wie tylko jedna funkcja, w których może śledzić informacje z poprzednich czasów jej uruchomienia.

W tym sensie pozwalają funkcjom zachowywać się trochę jak obiekty z prywatnymi atrybutami.

Pełny post:

Czym więc są te zamknięcia?


Czy w tym przykładzie można więc podkreślić główną zaletę zamknięć? Powiedzmy, że mam funkcję emailError (sendToAddress, errorString). Mogę wtedy powiedzieć, devError = emailError("devinrhode2@googmail.com", errorString)a następnie mam własną niestandardową wersję współdzielonej funkcji emailError?
Devin G. Rhode,

To wyjaśnienie i związany z nim doskonały przykład w linku do (zamknięcie rzeczy) jest najlepszym sposobem na zrozumienie zamknięć i powinno być na samej górze!
HopeKing

215

Zamknięcia są proste:

Poniższy prosty przykład obejmuje wszystkie główne punkty zamknięć JavaScript. *  

Oto fabryka produkująca kalkulatory, które mogą dodawać i rozmnażać:

function make_calculator() {
  var n = 0; // this calculator stores a single number n
  return {
    add: function(a) {
      n += a;
      return n;
    },
    multiply: function(a) {
      n *= a;
      return n;
    }
  };
}

first_calculator = make_calculator();
second_calculator = make_calculator();

first_calculator.add(3); // returns 3
second_calculator.add(400); // returns 400

first_calculator.multiply(11); // returns 33
second_calculator.multiply(10); // returns 4000

Kluczowy punkt: każde wezwanie do make_calculatorutworzenia nowej zmiennej lokalnej n, która będzie nadal użyteczna dla tego kalkulatora addi multiplydziała długo po make_calculatorpowrocie.

Jeśli jesteś zaznajomiony z ramkami stosu, te kalkulatory wydają się dziwne: Jak mogą uzyskać dostęp npo make_calculatorpowrocie? Odpowiedzią jest wyobrazić sobie, że JavaScript nie używa „ramek stosu”, ale zamiast tego używa „ramek sterty”, które mogą pozostać po wywołaniu funkcji, która je zwróciła.

Funkcje wewnętrzne, takie jak addi multiply, które mają zmienne dostępowe zadeklarowane w funkcji zewnętrznej ** , nazywane są zamknięciami .

To prawie wszystko, co jest do zamknięcia.



* Na przykład, obejmuje wszystkie punkty w artykule „Zamknięcia dla manekinów” podanym w innej odpowiedzi , z wyjątkiem przykładu 6, który po prostu pokazuje, że zmiennych można użyć przed ich zadeklarowaniem, co jest miłym faktem, ale całkowicie niezwiązanym z zamknięciami. Obejmuje również wszystkie punkty w zaakceptowanej odpowiedzi , z wyjątkiem punktów (1), które funkcje kopiują swoje argumenty do zmiennych lokalnych (nazwane argumenty funkcji), oraz (2), że kopiowanie liczb tworzy nowy numer, ale kopiuje odwołanie do obiektu daje kolejne odniesienie do tego samego obiektu. Są to również dobrze wiedzieć, ale znowu całkowicie niezwiązane z zamknięciami. Jest również bardzo podobny do przykładu w tej odpowiedzi, ale nieco krótszy i mniej abstrakcyjny. Nie obejmuje to punktuta odpowiedź lub komentarz , ponieważ JavaScript utrudnia podłączenie prąduwartość zmiennej pętli do funkcji wewnętrznej: Krok „podłączenie” można wykonać tylko przy pomocy funkcji pomocnika, która obejmuje twoją funkcję wewnętrzną i jest wywoływana przy każdej iteracji pętli. (Ściśle mówiąc, funkcja wewnętrzna uzyskuje dostęp do kopii zmiennej funkcji pomocnika, zamiast podłączania czegokolwiek.) Ponownie, bardzo przydatne podczas tworzenia zamknięć, ale nie jest częścią tego, co jest zamknięciem ani jak działa. Występuje dodatkowe zamieszanie, ponieważ zamknięcia działają inaczej w językach funkcjonalnych, takich jak ML, gdzie zmienne są powiązane raczej z wartościami niż z przestrzenią pamięci, zapewniając stały strumień ludzi, którzy rozumieją zamknięcia w pewien sposób (mianowicie „podłączanie”), czyli po prostu niepoprawny dla JavaScript, gdzie zmienne są zawsze powiązane z miejscem do przechowywania, a nigdy z wartościami.

** Każda funkcja zewnętrzna, jeśli kilka jest zagnieżdżonych lub nawet w kontekście globalnym, jak wyraźnie wskazuje ta odpowiedź .


Co by się stało, gdybyś wywołał: second_calculator = first_calculator (); zamiast second_calculator = make_calculator (); ? Powinno być tak samo, prawda?
Ronen Festinger

4
@Ronen: Ponieważ first_calculatorjest to obiekt (a nie funkcja), nie należy używać nawiasów second_calculator = first_calculator;, ponieważ jest to przypisanie, a nie wywołanie funkcji. Aby odpowiedzieć na twoje pytanie, byłoby tylko jedno wywołanie make_calculator, więc wykonano by tylko jeden kalkulator, a zmienne first_calculator i second_calculator odnosiłyby się do tego samego kalkulatora, więc odpowiedzi byłyby 3, 403, 4433, 44330.
Matt

204

Jak wytłumaczyłbym to sześciolatkowi:

Wiesz, jak dorośli mogą posiadać dom, a oni nazywają go domem? Kiedy mama ma dziecko, to tak naprawdę nic nie ma, prawda? Ale jego rodzice są właścicielami domu, więc za każdym razem, gdy ktoś pyta dziecko „Gdzie jest twój dom?”, Może odpowiedzieć „ten dom!” I wskazać dom jego rodziców. „Zamknięcie” to zdolność dziecka do tego, by zawsze (nawet za granicą) móc powiedzieć, że ma dom, mimo że tak naprawdę to rodzice są właścicielami domu.


200

Czy możesz wyjaśnić zamknięcia 5-latkowi? *

Nadal uważam, że wyjaśnienia Google działają bardzo dobrze i są zwięzłe:

/*
*    When a function is defined in another function and it
*    has access to the outer function's context even after
*    the outer function returns.
*
* An important concept to learn in JavaScript.
*/

function outerFunction(someNum) {
    var someString = 'Hey!';
    var content = document.getElementById('content');
    function innerFunction() {
        content.innerHTML = someNum + ': ' + someString;
        content = null; // Internet Explorer memory leak for DOM reference
    }
    innerFunction();
}

outerFunction(1);​

Dowód, że ten przykład tworzy zamknięcie, nawet jeśli funkcja wewnętrzna nie powróci

* Pytanie AC #


11
Kod jest „poprawny”, jako przykład zamknięcia, nawet jeśli nie odnosi się do części komentarza na temat używania zamknięcia po powrocie funkcji zewnętrznej. To nie jest świetny przykład. Istnieje wiele innych sposobów zamknięcia, które nie wymagają zwrotu funkcji wewnętrznej. np. funkcja wewnętrzna może być przekazana do innej funkcji, w której jest wywoływana natychmiast lub zapisywana i wywoływana jakiś czas później, i we wszystkich przypadkach ma dostęp do kontekstu funkcji zewnętrznej, który został utworzony, gdy została wywołana.
dlaliberte

6
@syockit Nie, Moss się myli. Zamknięcie jest tworzone niezależnie od tego, czy funkcja kiedykolwiek wymyka się zakresowi, w którym jest zdefiniowana, a bezwarunkowo utworzone odwołanie do środowiska leksykalnego rodzica powoduje, że wszystkie zmienne w zakresie nadrzędnym są dostępne dla wszystkich funkcji, niezależnie od tego, czy są wywoływane na zewnątrz czy wewnątrz zakres, w jakim zostały utworzone.
Asad Saeeduddin,

176

Lepiej uczę się przez porównania DOBRY / ZŁY. Lubię widzieć działający kod, a po nim niedziałający kod, który ktoś może napotkać. Ułożyła się jsFiddle że robi porównanie i stara się sprowadzić do różnic najprostszych wyjaśnień mogę wymyślić.

Zamknięcia wykonane poprawnie:

console.log('CLOSURES DONE RIGHT');

var arr = [];

function createClosure(n) {
    return function () {
        return 'n = ' + n;
    }
}

for (var index = 0; index < 10; index++) {
    arr[index] = createClosure(index);
}

for (var index in arr) {
    console.log(arr[index]());
}
  • W powyższym kodzie createClosure(n)wywoływany jest przy każdej iteracji pętli. Zauważ, że nazwałem zmienną, naby podkreślić, że jest to nowa zmienna utworzona w nowym zakresie funkcji i nie jest tą samą zmienną, indexktóra jest związana z zewnętrznym zakresem.

  • Tworzy to nowy zakres i njest związany z tym zakresem; oznacza to, że mamy 10 oddzielnych zakresów, po jednym dla każdej iteracji.

  • createClosure(n) zwraca funkcję, która zwraca nw tym zakresie.

  • W ramach każdego zakresu njest związany z dowolną wartością, jaką miał, kiedy createClosure(n)został wywołany, więc funkcja zagnieżdżona, która zostanie zwrócona, zawsze zwróci wartość tego n, co miała podczas createClosure(n)wywołania.

Zamknięcia wykonane nieprawidłowo:

console.log('CLOSURES DONE WRONG');

function createClosureArray() {
    var badArr = [];

    for (var index = 0; index < 10; index++) {
        badArr[index] = function () {
            return 'n = ' + index;
        };
    }
    return badArr;
}

var badArr = createClosureArray();

for (var index in badArr) {
    console.log(badArr[index]());
}
  • W powyższym kodzie pętla została przesunięta w obrębie createClosureArray()funkcji, a teraz funkcja zwraca tylko ukończoną tablicę, co na pierwszy rzut oka wydaje się bardziej intuicyjne.

  • To, co może nie być oczywiste, to fakt, że createClosureArray()wywoływane jest tylko wtedy, gdy dla tej funkcji tworzony jest tylko jeden zakres zamiast jednego dla każdej iteracji pętli.

  • W ramach tej funkcji indexdefiniowana jest zmienna o nazwie . Pętla działa i dodaje funkcje do zwracanej tablicy index. Zauważ, że indexjest zdefiniowany w createClosureArrayfunkcji, która jest wywoływana tylko raz.

  • Ponieważ w createClosureArray()funkcji był tylko jeden zakres , indexjest on związany tylko z wartością w tym zakresie. Innymi słowy, za każdym razem, gdy pętla zmienia wartość index, zmienia ją dla wszystkiego, co odnosi się do niej w tym zakresie.

  • Wszystkie funkcje dodane do tablicy zwracają indexzmienną SAME z zakresu nadrzędnego, w którym została zdefiniowana, zamiast 10 różnych z 10 różnych zakresów, takich jak pierwszy przykład. W rezultacie wszystkie 10 funkcji zwraca tę samą zmienną z tego samego zakresu.

  • Po zakończeniu i zakończeniu indexmodyfikacji pętli wartość końcowa wynosiła 10, dlatego każda funkcja dodana do tablicy zwraca wartość pojedynczej indexzmiennej, która jest teraz ustawiona na 10.

Wynik

ZAMKNIĘTE PRAWO
n = 0
n = 1
n = 2
n = 3
n = 4
n = 5
n = 6
n = 7
n = 8
n = 9

ZAMKNIĘTE WYKONANE ŹLE
n = 10
n = 10
n = 10
n = 10
n = 10
n = 10
n = 10
n = 10
n = 10
n = 10


1
Niezły dodatek, dzięki. Żeby było bardziej jasne, można sobie wyobrazić, w jaki sposób „zła” tablica jest tworzona w „złej” pętli z każdą iteracją: 1. iteracja: [function () {return 'n =' + 0;}] 2nd iteracja: [( function () {return 'n =' + 1;}), (function () {return 'n =' + 1;})] Trzecia iteracja: [(function () {return 'n =' + 2;}) , (function () {return 'n =' + 2;}), (function () {return 'n =' + 2;})] itd. Tak więc za każdym razem, gdy zmienia się wartość indeksu, jest odzwierciedlana we wszystkich funkcjach już dodany do tablicy.
Alex Alexeev

3
Używanie letdo varnaprawia różnicę.
Rupam Datta

Czy nie jest to, że „Zamknięcie wykonane prawidłowo” jest przykładem „zamknięcia wewnątrz zamknięcia”?
TechnicalSmile

Chodzi mi o to, że każda funkcja jest technicznie zamknięciem, ale ważną częścią jest to, że funkcja definiuje w niej nową zmienną. Funkcja, która się zwraca, zwraca tylko referencje nutworzone w nowym zamknięciu. Zwracamy funkcję, abyśmy mogli zapisać ją w tablicy i wywołać ją później.
Chev

Jeśli chcesz po prostu zapisać wynik w tablicy w pierwszej iteracji następnie można wbudować go w ten sposób: arr[index] = (function (n) { return 'n = ' + n; })(index);. Ale potem przechowujesz wynikowy ciąg w tablicy zamiast funkcji do wywołania, która pokonuje punkt mojego przykładu.
Chev

164

Wikipedia na temat zamknięć :

W informatyce zamknięcie jest funkcją wraz ze środowiskiem odniesienia dla nielokalnych nazw (wolnych zmiennych) tej funkcji.

Technicznie rzecz biorąc, w JavaScripcie , każda funkcja jest zamknięcie . Zawsze ma dostęp do zmiennych zdefiniowanych w otaczającym zakresie.

Ponieważ konstrukcja definiująca zakres w JavaScript jest funkcją , a nie blokiem kodu jak w wielu innych językach, to co zwykle rozumiemy przez zamknięcie w JavaScript to funkcja działająca ze zmiennymi nielokalnymi zdefiniowanymi w już wykonanej funkcji otaczającej .

Zamknięcia są często używane do tworzenia funkcji z ukrytymi prywatnymi danymi (ale nie zawsze tak jest).

var db = (function() {
    // Create a hidden object, which will hold the data
    // it's inaccessible from the outside.
    var data = {};

    // Make a function, which will provide some access to the data.
    return function(key, val) {
        if (val === undefined) { return data[key] } // Get
        else { return data[key] = val } // Set
    }
    // We are calling the anonymous surrounding function,
    // returning the above inner function, which is a closure.
})();

db('x')    // -> undefined
db('x', 1) // Set x to 1
db('x')    // -> 1
// It's impossible to access the data object itself.
// We are able to get or set individual it.

ems

W powyższym przykładzie użyto anonimowej funkcji, która została wykonana raz. Ale nie musi tak być. Można go nazwać (np. mkdb) I wykonać później, generując funkcję bazy danych przy każdym wywołaniu. Każda wygenerowana funkcja będzie miała swój własny ukryty obiekt bazy danych. Innym przykładem użycia zamknięć jest sytuacja, gdy nie zwracamy funkcji, ale obiekt zawierający wiele funkcji do różnych celów, z których każda ma dostęp do tych samych danych.


2
To najlepsze wytłumaczenie dla zamknięć JavaScript. Powinna być wybrana odpowiedź. Reszta jest wystarczająco zabawna, ale ta w rzeczywistości jest przydatna w praktyczny sposób dla koderów JavaScript z prawdziwego świata.
geoidesic

136

Przygotowałem interaktywny samouczek JavaScript, aby wyjaśnić, jak działają zamknięcia. Co to jest zamknięcie?

Oto jeden z przykładów:

var create = function (x) {
    var f = function () {
        return x; // We can refer to x here!
    };
    return f;
};
// 'create' takes one argument, creates a function

var g = create(42);
// g is a function that takes no arguments now

var y = g();
// y is 42 here

128

Dzieci zawsze będą pamiętać sekrety, które podzieliły się z rodzicami, nawet po ich odejściu. To są zamknięcia dla funkcji.

Sekretami funkcji JavaScript są zmienne prywatne

var parent = function() {
 var name = "Mary"; // secret
}

Za każdym razem, gdy go wywołujesz, tworzona jest zmienna lokalna „name”, której nadawana jest nazwa „Mary”. I za każdym razem, gdy funkcja wychodzi, zmienna zostaje utracona, a nazwa zostaje zapomniana.

Jak można się domyślić, ponieważ zmienne są tworzone ponownie przy każdym wywołaniu funkcji i nikt ich nie pozna, musi istnieć tajne miejsce, w którym są przechowywane. To może być nazywany Komnata Tajemnic lub stosu lub zakresu lokalnej , ale to naprawdę nie ma znaczenia. Wiemy, że gdzieś tam są ukryte w pamięci.

Ale w JavaScript jest ta szczególna rzecz, że funkcje tworzone wewnątrz innych funkcji mogą również znać lokalne zmienne swoich rodziców i utrzymywać je tak długo, jak żyją.

var parent = function() {
  var name = "Mary";
  var child = function(childName) {
    // I can also see that "name" is "Mary"
  }
}

Tak długo, jak jesteśmy w funkcji nadrzędnej, może ona tworzyć jedną lub więcej funkcji potomnych, które współużytkują tajne zmienne z tajnego miejsca.

Ale smutne jest to, że jeśli dziecko jest również prywatną zmienną funkcji rodzica, umrze również, gdy rodzic skończy, a sekrety umrą wraz z nimi.

Aby żyć, dziecko musi wyjść, zanim będzie za późno

var parent = function() {
  var name = "Mary";
  var child = function(childName) {
    return "My name is " + childName  +", child of " + name; 
  }
  return child; // child leaves the parent ->
}
var child = parent(); // < - and here it is outside 

A teraz, chociaż Maryja „już nie biegnie”, pamięć o niej nie zostaje utracona, a jej dziecko zawsze pamięta swoje imię i inne tajemnice, które dzielili podczas wspólnego pobytu.

Jeśli więc nazwiesz dziecko „Alice”, ona odpowie

child("Alice") => "My name is Alice, child of Mary"

To wszystko, co można powiedzieć.


15
To wyjaśnienie było dla mnie najbardziej sensowne, ponieważ nie zakłada znaczącej wcześniejszej znajomości terminów technicznych. Najważniejsze wyjaśnienie tutaj zakłada, że ​​osoba, która nie rozumie zamknięć, ma pełne i pełne zrozumienie terminów, takich jak „zakres leksykalny” i „kontekst wykonania” - chociaż rozumiem je koncepcyjnie, nie sądzę, że jestem tak czuję się dobrze z ich szczegółami takimi, jakimi powinienem być, a wyjaśnienie bez żargonu jest tym, co sprawiło, że zamknięcia w końcu mnie kliknęły, dziękuję. Jako bonus myślę, że wyjaśnia również bardzo zwięźle zakres.
Emma W

103

Nie rozumiem, dlaczego odpowiedzi są tutaj tak złożone.

Oto zamknięcie:

var a = 42;

function b() { return a; }

Tak. Prawdopodobnie używasz tego wiele razy dziennie.


Nie ma powodu, aby sądzić, że zamknięcia są złożonym hakowaniem projektowym w celu rozwiązania konkretnych problemów. Nie, w zamknięciach chodzi tylko o użycie zmiennej, która pochodzi z wyższego zakresu z punktu widzenia miejsca, w którym funkcja została zadeklarowana (nie uruchomiona) .

To, co pozwala ci robić, może być bardziej spektakularne, zobacz inne odpowiedzi.


5
Wydaje się, że ta odpowiedź nie pomoże w zdezorientowaniu ludzi. A rough odpowiednik w tradycyjnym języku programowania może być stworzenie b () jako metoda na obiekcie, który również posiada prywatny stałej lub mienia a. Moim zdaniem, niespodzianką jest to, że obiekt zakresu JS skutecznie zapewnia araczej właściwość niż stałą. I zauważysz to ważne zachowanie tylko, jeśli je zmodyfikujesz, jak wreturn a++;
Jon Coombs

1
Dokładnie to, co powiedział Jon. Zanim w końcu pomyślałem o zamknięciach, miałem trudności ze znalezieniem praktycznych przykładów. Tak, floribon stworzył zamknięcie, ale niewykształcenie mnie nie nauczyłoby mnie absolutnie niczego.
Chev

3
Nie definiuje to, czym jest zamknięcie - jest to tylko przykład, który go używa. I nie zajmuje się niuansami tego, co dzieje się, gdy kończy się zakres; Nie sądzę, żeby ktokolwiek miał pytanie o zakres leksykalny, gdy wszystkie zakresy są jeszcze w pobliżu, a zwłaszcza w przypadku zmiennej globalnej.
Gerard ONeill,

91

Przykład pierwszego punktu autorstwa dlaliberte:

Zamknięcie jest tworzone nie tylko po zwróceniu funkcji wewnętrznej. W rzeczywistości funkcja zamykająca wcale nie musi zwracać. Zamiast tego możesz przypisać swoją funkcję wewnętrzną do zmiennej w zakresie zewnętrznym lub przekazać ją jako argument innej funkcji, z której mogłaby zostać natychmiast wykorzystana. Dlatego zamknięcie funkcji zamykającej prawdopodobnie istnieje już w momencie wywołania funkcji zamykającej, ponieważ każda funkcja wewnętrzna ma do niej dostęp natychmiast po jej wywołaniu.

var i;
function foo(x) {
    var tmp = 3;
    i = function (y) {
        console.log(x + y + (++tmp));
    }
}
foo(2);
i(3);

Małe wyjaśnienie dotyczące możliwej niejednoznaczności. Kiedy powiedziałem „W rzeczywistości funkcja zamykająca wcale nie musi wracać”. Nie miałem na myśli „nie zwracaj żadnej wartości”, ale „wciąż aktywny”. Tak więc przykład nie pokazuje tego aspektu, chociaż pokazuje inny sposób, w jaki funkcję wewnętrzną można przekazać do zakresu zewnętrznego. Głównym punktem, który próbowałem zrobić, jest czas utworzenia zamknięcia (dla funkcji zamykającej), ponieważ niektórzy ludzie myślą, że dzieje się to po powrocie funkcji zamykającej. Innym przykładem jest wymagane, aby pokazać, że zamknięcie jest tworzony, gdy funkcja jest nazywana .
dlaliberte

88

Zamknięcie ma miejsce, gdy funkcja wewnętrzna ma dostęp do zmiennych w swojej funkcji zewnętrznej. To prawdopodobnie najprostsze jedno-liniowe wyjaśnienie, jakie można uzyskać w przypadku zamknięć.


35
To tylko połowa wyjaśnienia. Ważną rzeczą, o której należy pamiętać w przypadku zamknięć, jest to, że jeśli funkcja wewnętrzna jest nadal używana po wyjściu z funkcji zewnętrznej, stare wartości funkcji zewnętrznej są nadal dostępne dla funkcji wewnętrznej.
pcorcoran

22
Właściwie to nie stare wartości funkcji zewnętrznej są dostępne dla funkcji wewnętrznej, ale stare zmienne , które mogą mieć nowe wartości, jeśli jakaś funkcja mogłaby je zmienić.
dlaliberte

86

Wiem, że istnieje już wiele rozwiązań, ale sądzę, że ten mały i prosty skrypt może być przydatny do zademonstrowania koncepcji:

// makeSequencer will return a "sequencer" function
var makeSequencer = function() {
    var _count = 0; // not accessible outside this function
    var sequencer = function () {
        return _count++;
    }
    return sequencer;
}

var fnext = makeSequencer();
var v0 = fnext();     // v0 = 0;
var v1 = fnext();     // v1 = 1;
var vz = fnext._count // vz = undefined

82

Przespałeś się i zaprosiłeś Dana. Mówisz Danowi, żeby przyniósł jeden kontroler XBox.

Dan zaprasza Paula. Dan prosi Paula, aby przyniósł jednego kontrolera. Ilu kontrolerów przywieziono na imprezę?

function sleepOver(howManyControllersToBring) {

    var numberOfDansControllers = howManyControllersToBring;

    return function danInvitedPaul(numberOfPaulsControllers) {
        var totalControllers = numberOfDansControllers + numberOfPaulsControllers;
        return totalControllers;
    }
}

var howManyControllersToBring = 1;

var inviteDan = sleepOver(howManyControllersToBring);

// The only reason Paul was invited is because Dan was invited. 
// So we set Paul's invitation = Dan's invitation.

var danInvitedPaul = inviteDan(howManyControllersToBring);

alert("There were " + danInvitedPaul + " controllers brought to the party.");

80

Autor „ Zamknięć” całkiem dobrze wyjaśnił zamknięcia, wyjaśniając powód, dla którego ich potrzebujemy, a także wyjaśniając środowisko leksykalne, które jest niezbędne do zrozumienia zamknięć.
Oto podsumowanie:

Co się stanie, jeśli zmienna jest dostępna, ale nie jest lokalna? Jak tutaj:

Wpisz opis zdjęcia tutaj

W takim przypadku interpreter znajduje zmienną w LexicalEnvironmentobiekcie zewnętrznym .

Proces składa się z dwóch etapów:

  1. Po pierwsze, gdy funkcja f jest tworzona, nie jest tworzona w pustej przestrzeni. Istnieje bieżący obiekt LexicalEnvironment. W powyższym przypadku jest to okno (a jest niezdefiniowane w momencie tworzenia funkcji).

Wpisz opis zdjęcia tutaj

Gdy funkcja jest tworzona, otrzymuje ukrytą właściwość o nazwie [[Zakres]], która odwołuje się do bieżącego LexicalEnvironment.

Wpisz opis zdjęcia tutaj

Jeśli zmienna zostanie odczytana, ale nigdzie nie można jej znaleźć, generowany jest błąd.

Funkcje zagnieżdżone

Funkcje można zagnieżdżać jeden w drugim, tworząc łańcuch środowisk LexicalEnvironments, który można również nazwać łańcuchem zasięgu.

Wpisz opis zdjęcia tutaj

Tak więc funkcja g ma dostęp do g, a i f.

Domknięcia

Funkcja zagnieżdżona może nadal działać po zakończeniu funkcji zewnętrznej:

Wpisz opis zdjęcia tutaj

Oznaczanie środowisk leksykalnych:

Wpisz opis zdjęcia tutaj

Jak widzimy, this.sayjest właściwością w obiekcie użytkownika, więc nadal działa po zakończeniu użytkownika.

A jeśli pamiętasz, kiedy this.sayjest tworzony, to (jak każda funkcja) otrzymuje wewnętrzne odniesienie this.say.[[Scope]]do bieżącego LexicalEnvironment. Tak więc środowisko leksykalne bieżącego wykonania użytkownika pozostaje w pamięci. Wszystkie zmienne użytkownika również są jego właściwościami, więc są one również starannie przechowywane, a nie jak zwykle usuwane.

Chodzi o to, aby zapewnić, że jeśli funkcja wewnętrzna chce uzyskać dostęp do zmiennej zewnętrznej w przyszłości, jest w stanie to zrobić.

Podsumowując:

  1. Funkcja wewnętrzna zachowuje odniesienie do zewnętrznego środowiska leksykalnego.
  2. Funkcja wewnętrzna może uzyskać dostęp do zmiennych w dowolnym momencie, nawet jeśli funkcja zewnętrzna jest zakończona.
  3. Przeglądarka przechowuje LexicalEnvironment i wszystkie jego właściwości (zmienne) w pamięci, dopóki nie pojawi się wewnętrzna funkcja, która się do niego odwołuje.

To się nazywa zamknięcie.


78

Funkcje JavaScript mogą uzyskać dostęp do:

  1. Argumenty
  2. Locals (tzn. Ich zmienne lokalne i funkcje lokalne)
  3. Środowisko, które obejmuje:
    • globals, w tym DOM
    • cokolwiek w funkcjach zewnętrznych

Jeśli funkcja uzyskuje dostęp do swojego środowiska, oznacza to zamknięcie.

Pamiętaj, że funkcje zewnętrzne nie są wymagane, ale oferują korzyści, których nie omawiam tutaj. Dzięki dostępowi do danych w jego otoczeniu zamknięcie utrzymuje te dane przy życiu. W podklasie funkcji zewnętrznych / wewnętrznych funkcja zewnętrzna może tworzyć dane lokalne i ostatecznie wychodzić, a jednak, jeśli jakakolwiek funkcja wewnętrzna (funkcje) przetrwają po wyjściu funkcji zewnętrznej, wówczas funkcja (funkcje wewnętrzne) zachowują lokalne dane funkcji zewnętrznej żywy.

Przykład zamknięcia korzystającego ze środowiska globalnego:

Wyobraź sobie, że zdarzenia przycisku Przepełnienie stosu Głosuj w górę i Głosuj w dół są implementowane jako zamknięcia, głosowanie w górę i głosowanie w dół, które mają dostęp do zmiennych zewnętrznych isVotedUp i isVotedDown, które są zdefiniowane globalnie. (Dla uproszczenia mam na myśli przyciski pytań głosowania StackOverflow, a nie tablicę przycisków odpowiedzi głosowaniem).

Gdy użytkownik kliknie przycisk Głosowania, funkcja głosowania sprawdza, czy isVotedDown == true, aby ustalić, czy głosować w górę, czy po prostu anulować głosowanie w dół. Funkcja głosowania U__kliknij jest zamknięciem, ponieważ uzyskuje dostęp do swojego środowiska.

var isVotedUp = false;
var isVotedDown = false;

function voteUp_click() {
  if (isVotedUp)
    return;
  else if (isVotedDown)
    SetDownVote(false);
  else
    SetUpVote(true);
}

function voteDown_click() {
  if (isVotedDown)
    return;
  else if (isVotedUp)
    SetUpVote(false);
  else
    SetDownVote(true);
}

function SetUpVote(status) {
  isVotedUp = status;
  // Do some CSS stuff to Vote-Up button
}

function SetDownVote(status) {
  isVotedDown = status;
  // Do some CSS stuff to Vote-Down button
}

Wszystkie cztery z tych funkcji są zamknięciami, ponieważ wszystkie mają dostęp do swojego środowiska.


59

Jako ojciec 6-latka, który obecnie uczy małych dzieci (i względnego nowicjusza w kodowaniu bez formalnego wykształcenia, więc konieczne będą poprawki), myślę, że lekcja najlepiej trzymałaby się praktycznej zabawy. Jeśli sześciolatek jest gotowy zrozumieć, co to jest zamknięcie, to są one na tyle duże, że same mogą spróbować. Sugerowałbym wklejenie kodu do jsfiddle.net, wyjaśnienie i pozostawienie ich samych w celu stworzenia wyjątkowej piosenki. Poniższy tekst wyjaśniający jest prawdopodobnie bardziej odpowiedni dla 10-latka.

function sing(person) {

    var firstPart = "There was " + person + " who swallowed ";

    var fly = function() {
        var creature = "a fly";
        var result = "Perhaps she'll die";
        alert(firstPart + creature + "\n" + result);
    };

    var spider = function() {
        var creature = "a spider";
        var result = "that wiggled and jiggled and tickled inside her";
        alert(firstPart + creature + "\n" + result);
    };

    var bird = function() {
        var creature = "a bird";
        var result = "How absurd!";
        alert(firstPart + creature + "\n" + result);
    };

    var cat = function() {
        var creature = "a cat";
        var result = "Imagine That!";
        alert(firstPart + creature + "\n" + result);
    };

    fly();
    spider();
    bird();
    cat();
}

var person="an old lady";

sing(person);

INSTRUKCJE

DANE: Dane to zbiór faktów. Mogą to być liczby, słowa, pomiary, obserwacje, a nawet tylko opisy rzeczy. Nie możesz go dotknąć, powąchać ani posmakować. Możesz to zapisać, wypowiedzieć i usłyszeć. Możesz go użyć do stworzenia zapachu i smaku dotykowego za pomocą komputera. Może być przydatny przez komputer za pomocą kodu.

KOD: Cały powyższy zapis nazywa się kodem . Jest napisany w JavaScript.

JAVASCRIPT: JavaScript jest językiem. Podobnie jak angielski, francuski lub chiński to języki. Istnieje wiele języków, które są rozumiane przez komputery i inne procesory elektroniczne. Aby JavaScript mógł być zrozumiany przez komputer, potrzebuje tłumacza. Wyobraź sobie, że nauczyciel, który mówi tylko po rosyjsku, przychodzi uczyć twoich uczniów w szkole. Kiedy nauczyciel mówi „все садятся”, klasa nie zrozumie. Ale na szczęście masz w klasie rosyjskiego ucznia, który mówi wszystkim, że to znaczy „wszyscy usiądźcie” - więc wszyscy to robicie. Klasa jest jak komputer, a rosyjski uczeń jest tłumaczem. W przypadku JavaScript najpopularniejszym tłumaczem jest przeglądarka.

PRZEGLĄDARKA: Kiedy łączysz się z Internetem na komputerze, tablecie lub telefonie, aby odwiedzić stronę internetową, korzystasz z przeglądarki. Przykłady, które możesz znać, to Internet Explorer, Chrome, Firefox i Safari. Przeglądarka może zrozumieć JavaScript i powiedzieć komputerowi, co musi zrobić. Instrukcje JavaScript są nazywane funkcjami.

FUNKCJA: Funkcja w JavaScript jest jak fabryka. Może to być mała fabryka z tylko jedną maszyną w środku. Lub może zawierać wiele innych małych fabryk, każda z wieloma maszynami wykonującymi różne zadania. W prawdziwej fabryce ubrań możesz mieć do czynienia z ryzami materiału i szpulkami nici oraz z koszulkami i dżinsami. Nasza fabryka JavaScript przetwarza tylko dane, nie może szyć, wiercić dziury ani topić metalu. W naszej fabryce JavaScript dane wchodzą i wychodzą.

Wszystkie te dane brzmią trochę nudno, ale jest naprawdę bardzo fajne; możemy mieć funkcję, która mówi robotowi, co zrobić na obiad. Powiedzmy, że zapraszam ciebie i twojego przyjaciela do mojego domu. Najbardziej lubisz udka z kurczaka, lubię kiełbaski, twój przyjaciel zawsze chce tego, co chcesz, a mój przyjaciel nie je mięsa.

Nie mam czasu na zakupy, więc funkcja musi wiedzieć, co mamy w lodówce, aby podejmować decyzje. Każdy składnik ma inny czas gotowania i chcemy, aby robot podawał wszystko na gorąco jednocześnie. Musimy dostarczyć tej funkcji dane o tym, co lubimy, funkcja może „rozmawiać” z lodówką, a funkcja może kontrolować robota.

Funkcja zwykle ma nazwę, nawiasy i nawiasy klamrowe. Lubię to:

function cookMeal() {  /*  STUFF INSIDE THE FUNCTION  */  }

Zauważ, że /*...*/i //przestań czytać kod przez przeglądarkę.

NAZWA: Możesz wywołać funkcję niemal z dowolnego słowa, które chcesz. Przykład „cookMeal” jest typowy dla połączenia dwóch słów razem i nadania drugiemu dużej litery na początku - ale nie jest to konieczne. Nie może mieć spacji i nie może być liczbą samą w sobie.

RODZICE: „Nawiasy” lub ()skrzynka na listy w drzwiach fabryki funkcji JavaScript lub skrzynka pocztowa na ulicy do wysyłania pakietów informacji do fabryki. Czasami skrzynka pocztowa może być na przykład oznaczona cookMeal(you, me, yourFriend, myFriend, fridge, dinnerTime), w którym to przypadku wiesz, jakie dane musisz podać.

SZACIONKI: „Szelki”, które wyglądają tak, to {}przyciemnione szyby naszej fabryki. Z wnętrza fabryki widać, ale z zewnątrz nie widać.

PRZYKŁAD POWYŻSZEGO KODU

Nasz kod zaczyna się od funkcji słowa , więc wiemy, że jest jedna! Następnie nazwa funkcji śpiewa - to mój własny opis tego, o czym jest funkcja. Następnie nawiasy () . Nawiasy są zawsze dostępne dla funkcji. Czasami są one puste, a czasami mają coś w tym jeden ma słowa w.: (person). Po tym jest taki nawias klamrowy {. Oznacza to początek funkcji sing () . Ma partnera, który oznacza koniec sing () w ten sposób}

function sing(person) {  /* STUFF INSIDE THE FUNCTION */  }

Ta funkcja może mieć coś wspólnego ze śpiewaniem i może wymagać pewnych danych o osobie. Zawiera instrukcje, jak zrobić coś z tymi danymi.

Teraz, po funkcji sing () , pod koniec kodu jest linia

var person="an old lady";

ZMIENNE: Litery var oznaczają „zmienna”. Zmienna jest jak obwiednia. Na zewnątrz ta koperta jest oznaczona „osoba”. W środku znajduje się kartka papieru z informacją, której potrzebuje nasza funkcja, niektóre litery i spacje połączone razem jak kawałek sznurka (zwanego sznurkiem), który tworzy frazę „starsza pani”. Nasza koperta może zawierać inne rzeczy, takie jak liczby (nazywane liczbami całkowitymi), instrukcje (nazywane funkcjami), listy (nazywane tablicami ). Ponieważ ta zmienna jest zapisywana poza wszystkimi nawiasami klamrowymi {}i ponieważ możesz zobaczyć przez przyciemnione okna, gdy jesteś w nawiasach klamrowych, ta zmienna jest widoczna z dowolnego miejsca w kodzie. Nazywamy to „zmienną globalną”.

ZMIENNA GLOBALNA: osoba jest zmienną globalną, co oznacza, że ​​jeśli zmienisz jej wartość z „starszej damy” na „młodego mężczyznę”, osoba ta pozostanie młodym mężczyzną, dopóki nie zdecydujesz się go zmienić ponownie i że każda inna funkcja w kod widzi, że to młody człowiek. Naciśnij F12przycisk lub spójrz na ustawienia Opcje, aby otworzyć konsolę programisty przeglądarki i wpisz „osoba”, aby zobaczyć, jaka jest ta wartość. Wpisz, person="a young man"aby go zmienić, a następnie ponownie wpisz „osoba”, aby zobaczyć, że zmienił się.

Po tym mamy linię

sing(person);

Ta linia wywołuje funkcję, tak jakby to był pies

„Chodź, zaśpiewaj , chodź i zdobądź osobę !”

Gdy przeglądarka załaduje kod JavaScript i osiągnie ten wiersz, uruchomi funkcję. Kładę wiersz na końcu, aby upewnić się, że przeglądarka ma wszystkie informacje potrzebne do jej uruchomienia.

Funkcje definiują akcje - główna funkcja polega na śpiewaniu. Zawiera zmienną o nazwie firstPart, która odnosi się do śpiewu o osobie, która dotyczy każdego z wierszy utworu: „Była” + osoba + ”, która przełknęła”. Jeśli wpiszesz firstPart w konsoli , nie otrzymasz odpowiedzi, ponieważ zmienna jest zablokowana w funkcji - przeglądarka nie widzi wewnątrz przyciemnionych okien nawiasów klamrowych.

ZAMKNIĘCIA: Zamknięcia to mniejsze funkcje znajdujące się w funkcji big sing () . Małe fabryki w dużej fabryce. Każdy z nich ma własne nawiasy klamrowe, co oznacza, że ​​zmiennych w nich nie można zobaczyć z zewnątrz. Dlatego nazwy zmiennych ( stworzenie i wynik ) mogą być powtarzane w zamknięciach, ale z różnymi wartościami. Jeśli wpiszesz te nazwy zmiennych w oknie konsoli, nie uzyskasz ich wartości, ponieważ są ukryte przez dwie warstwy przyciemnionego okna.

Wszystkie zamknięcia wiedzą, czym jest zmienna funkcji sing () o nazwie firstPart , ponieważ mogą widzieć z ich przyciemnionych okien.

Po zamknięciach pojawiają się linie

fly();
spider();
bird();
cat();

Funkcja sing () wywoła każdą z tych funkcji w podanej kolejności. Następnie praca funkcji sing () zostanie zakończona.


56

Dobra, rozmawiając z 6-letnim dzieckiem, prawdopodobnie użyłbym następujących skojarzeń.

Wyobraź sobie - bawisz się ze swoimi młodszymi braćmi i siostrami w całym domu, poruszasz się z zabawkami i przyprowadzasz niektóre z nich do pokoju starszego brata. Po chwili twój brat wrócił ze szkoły i poszedł do swojego pokoju, a on zamknął się w nim, więc teraz nie można było uzyskać bezpośredniego dostępu do pozostawionych tam zabawek. Ale możesz zapukać do drzwi i poprosić brata o te zabawki. Nazywa się to zamknięciem zabawki ; twój brat nadrobił to dla ciebie, a on jest teraz poza zasięgiem .

Porównaj z sytuacją, gdy drzwi były zamknięte przez przeciąg i nikt nie był w środku (wykonanie funkcji ogólnej), a następnie doszło do lokalnego pożaru i spalenia pomieszczenia (pojemnik na śmieci: D), a następnie zbudowano nowe pomieszczenie i teraz możesz wyjść inne zabawki (nowa instancja funkcji), ale nigdy nie otrzymuj tych samych zabawek, które pozostały w instancji pierwszego pokoju.

Dla zaawansowanego dziecka umieściłbym coś takiego. Nie jest idealny, ale sprawia, że ​​czujesz, co to jest:

function playingInBrothersRoom (withToys) {
  // We closure toys which we played in the brother's room. When he come back and lock the door
  // your brother is supposed to be into the outer [[scope]] object now. Thanks god you could communicate with him.
  var closureToys = withToys || [],
      returnToy, countIt, toy; // Just another closure helpers, for brother's inner use.

  var brotherGivesToyBack = function (toy) {
    // New request. There is not yet closureToys on brother's hand yet. Give him a time.
    returnToy = null;
    if (toy && closureToys.length > 0) { // If we ask for a specific toy, the brother is going to search for it.

      for ( countIt = closureToys.length; countIt; countIt--) {
        if (closureToys[countIt - 1] == toy) {
          returnToy = 'Take your ' + closureToys.splice(countIt - 1, 1) + ', little boy!';
          break;
        }
      }
      returnToy = returnToy || 'Hey, I could not find any ' + toy + ' here. Look for it in another room.';
    }
    else if (closureToys.length > 0) { // Otherwise, just give back everything he has in the room.
      returnToy = 'Behold! ' + closureToys.join(', ') + '.';
      closureToys = [];
    }
    else {
      returnToy = 'Hey, lil shrimp, I gave you everything!';
    }
    console.log(returnToy);
  }
  return brotherGivesToyBack;
}
// You are playing in the house, including the brother's room.
var toys = ['teddybear', 'car', 'jumpingrope'],
    askBrotherForClosuredToy = playingInBrothersRoom(toys);

// The door is locked, and the brother came from the school. You could not cheat and take it out directly.
console.log(askBrotherForClosuredToy.closureToys); // Undefined

// But you could ask your brother politely, to give it back.
askBrotherForClosuredToy('teddybear'); // Hooray, here it is, teddybear
askBrotherForClosuredToy('ball'); // The brother would not be able to find it.
askBrotherForClosuredToy(); // The brother gives you all the rest
askBrotherForClosuredToy(); // Nothing left in there

Jak widać, zabawki pozostawione w pokoju są nadal dostępne dla brata i bez względu na to, czy pokój jest zamknięty. Oto jsbin do zabawy.


49

Odpowiedź dla sześciolatka (zakładając, że wie, czym jest funkcja, czym jest zmienna i jakie dane):

Funkcje mogą zwracać dane. Jednym rodzajem danych, które możesz zwrócić z funkcji, jest inna funkcja. Gdy ta nowa funkcja zostanie zwrócona, wszystkie zmienne i argumenty użyte w funkcji, która ją utworzyła, nie znikają. Zamiast tego ta funkcja nadrzędna „zamyka się”. Innymi słowy, nic nie może zajrzeć do jego wnętrza i zobaczyć użytych zmiennych, z wyjątkiem funkcji, którą zwrócił. Ta nowa funkcja ma specjalną możliwość spojrzenia wstecz na funkcję, która ją utworzyła i zobaczenia danych w niej zawartych.

function the_closure() {
  var x = 4;
  return function () {
    return x; // Here, we look back inside the_closure for the value of x
  }
}

var myFn = the_closure();
myFn(); //=> 4

Innym bardzo prostym sposobem wyjaśnienia tego jest zakres:

Za każdym razem, gdy utworzysz mniejszy zakres w większym zakresie, mniejszy zakres zawsze będzie mógł zobaczyć, co jest w większym zakresie.


49

Funkcja w JavaScript to nie tylko odwołanie do zestawu instrukcji (jak w języku C), ale zawiera również ukrytą strukturę danych, która składa się z odwołań do wszystkich używanych zmiennych nielokalnych (zmienne przechwycone). Takie dwuczęściowe funkcje nazywane są zamknięciami. Każda funkcja w JavaScript może być uważana za zamknięcie.

Zamknięcia są funkcjami ze stanem. Jest nieco podobny do „tego” w tym sensie, że „to” zapewnia również stan funkcji, ale funkcja i „to” są osobnymi obiektami („to” jest tylko fantazyjnym parametrem i jedynym sposobem na powiązanie go na stałe z funkcja polega na utworzeniu zamknięcia). Chociaż „to” i funkcja zawsze działają osobno, nie można oddzielić funkcji od jej zamknięcia, a język nie zapewnia dostępu do przechwyconych zmiennych.

Ponieważ wszystkie te zmienne zewnętrzne, do których odwołuje się funkcja zagnieżdżona leksykalnie, są w rzeczywistości zmiennymi lokalnymi w łańcuchu jej funkcji leksykalnie obejmujących (można przyjąć, że zmienne globalne są zmiennymi lokalnymi niektórych funkcji root), a każde pojedyncze wykonanie funkcji tworzy nowe wystąpienia z jego zmiennych lokalnych wynika, że ​​każde wykonanie funkcji zwracającej (lub w inny sposób ją przenoszącej, takiej jak rejestracja jako wywołanie zwrotne) funkcja zagnieżdżona tworzy nowe zamknięcie (z własnym potencjalnie unikalnym zestawem zmiennych nielokalnych, które reprezentują jej wykonanie kontekst).

Należy również zrozumieć, że zmienne lokalne w JavaScript są tworzone nie w ramce stosu, ale na stercie i niszczone tylko wtedy, gdy nikt się do nich nie odwołuje. Gdy funkcja powraca, odwołania do jej zmiennych lokalnych są zmniejszane, ale nadal mogą mieć wartość inną niż null, jeśli podczas bieżącego wykonywania staną się częścią zamknięcia i nadal będą się do nich odwoływać jej funkcje zagnieżdżone leksykalnie (co może się zdarzyć tylko wtedy, gdy odwołania do te zagnieżdżone funkcje zostały zwrócone lub w inny sposób przeniesione do jakiegoś zewnętrznego kodu).

Przykład:

function foo (initValue) {
   //This variable is not destroyed when the foo function exits.
   //It is 'captured' by the two nested functions returned below.
   var value = initValue;

   //Note that the two returned functions are created right now.
   //If the foo function is called again, it will return
   //new functions referencing a different 'value' variable.
   return {
       getValue: function () { return value; },
       setValue: function (newValue) { value = newValue; }
   }
}

function bar () {
    //foo sets its local variable 'value' to 5 and returns an object with
    //two functions still referencing that local variable
    var obj = foo(5);

    //Extracting functions just to show that no 'this' is involved here
    var getValue = obj.getValue;
    var setValue = obj.setValue;

    alert(getValue()); //Displays 5
    setValue(10);
    alert(getValue()); //Displays 10

    //At this point getValue and setValue functions are destroyed
    //(in reality they are destroyed at the next iteration of the garbage collector).
    //The local variable 'value' in the foo is no longer referenced by
    //anything and is destroyed too.
}

bar();

47

Być może trochę ponad wszystko, z wyjątkiem najbardziej przedwczesnych z sześciolatków, ale kilka przykładów, które pomogły mi sprawić, aby koncepcja zamknięcia w JavaScript została dla mnie kliknięta.

Zamknięcie to funkcja, która ma dostęp do zakresu innej funkcji (jej zmiennych i funkcji). Najłatwiejszym sposobem utworzenia zamknięcia jest użycie funkcji w ramach funkcji; dlatego, że w JavaScript funkcja zawsze ma dostęp do zakresu funkcji zawierającej.

function outerFunction() {
    var outerVar = "monkey";
    
    function innerFunction() {
        alert(outerVar);
    }
    
    innerFunction();
}

outerFunction();

ALERT: małpa

W powyższym przykładzie wywoływana jest funkcja zewnętrzna, która z kolei wywołuje funkcję wewnętrzną. Zwróć uwagę, w jaki sposób externalVar jest dostępny dla innerFunction, o czym świadczy prawidłowe alarmowanie o wartości outerVar.

Teraz rozważ następujące kwestie:

function outerFunction() {
    var outerVar = "monkey";
    
    function innerFunction() {
        return outerVar;
    }
    
    return innerFunction;
}

var referenceToInnerFunction = outerFunction();
alert(referenceToInnerFunction());

ALERT: małpa

referenceToInnerFunction jest ustawiony na outerFunction (), który po prostu zwraca odwołanie do innerFunction. Gdy wywoływana jest funkcja referenceToInnerFunction, zwraca wartość externalVar. Ponownie, jak powyżej, pokazuje to, że innerFunction ma dostęp do outerVar, zmiennej zewnętrznej funkcji. Co więcej, warto zauważyć, że zachowuje ten dostęp nawet po zakończeniu działania zewnętrznej funkcji.

I tutaj sprawy stają się naprawdę interesujące. Jeśli mielibyśmy pozbyć się externalFunction, powiedzmy, ustaw go na null, możesz pomyśleć, że referenToInnerFunction utraciłby dostęp do wartości outerVar. Ale tak nie jest.

function outerFunction() {
    var outerVar = "monkey";
    
    function innerFunction() {
        return outerVar;
    }
    
    return innerFunction;
}

var referenceToInnerFunction = outerFunction();
alert(referenceToInnerFunction());

outerFunction = null;
alert(referenceToInnerFunction());

ALERT: małpa ALERT: małpa

Ale jak to się dzieje? W jaki sposób referenToInnerFunction może nadal znać wartość outerVar, skoro externalFunction ma wartość null?

Powodem, dla którego referenceToInnerFunction może nadal uzyskiwać dostęp do wartości outerVar, jest to, że kiedy zamknięcie zostało utworzone po raz pierwszy przez umieszczenie wewnętrznej funkcji wewnątrz funkcji zewnętrznej, funkcja wewnętrzna dodała odniesienie do zakresu funkcji zewnętrznej (jej zmiennych i funkcji) do łańcucha zasięgu. Oznacza to, że innerFunction ma wskaźnik lub odniesienie do wszystkich zmiennych outerFunction, w tym outerVar. Tak więc nawet po zakończeniu wykonywania funkcji zewnętrznej lub nawet po jej usunięciu lub ustawieniu wartości NULL, zmienne w jej zakresie, takie jak outerVar, pozostają w pamięci z powodu wyjątkowego odwołania do nich ze strony funkcji wewnętrznej, do której zwrócono wartość referenceToInnerFunction. Aby naprawdę uwolnić outerVar i resztę zmiennych outerFunction z pamięci, musiałbyś pozbyć się tych wyjątkowych odniesień do nich,

//////////

Dwie inne rzeczy na temat zamknięć, na które należy zwrócić uwagę. Po pierwsze, zamknięcie zawsze będzie miało dostęp do ostatnich wartości jego zawierającej funkcji.

function outerFunction() {
    var outerVar = "monkey";
    
    function innerFunction() {
        alert(outerVar);
    }
    
    outerVar = "gorilla";

    innerFunction();
}

outerFunction();

ALERT: goryl

Po drugie, po utworzeniu zamknięcia zachowuje on odwołanie do wszystkich zmiennych i funkcji swojej funkcji zamykającej; nie można wybierać. A jednak zamknięcia powinny być stosowane oszczędnie, a przynajmniej ostrożnie, ponieważ mogą wymagać dużej ilości pamięci; wiele zmiennych może być przechowywanych w pamięci długo po zakończeniu wykonywania funkcji zawierającej.


45

Po prostu wskazałbym je na stronie Zamknięcia Mozilli . To najlepsze, najbardziej zwięzłe i proste wyjaśnienie podstaw zamknięcia i praktycznego zastosowania, jakie znalazłem. Jest wysoce zalecane każdemu, kto uczy się JavaScript.

I tak, poleciłbym to nawet sześciolatkowi - jeśli sześciolatek uczy się o zamknięciach, to logiczne, że są gotowi zrozumieć zwięzłe i proste wyjaśnienie podane w artykule.


Zgadzam się: wspomniana strona Mozilli jest szczególnie prosta i zwięzła. O dziwo, twój post nie został tak doceniony jak inni.
Brice Coustillas
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.