Mój przyjaciel i ja obecnie dyskutujemy, co jest zamknięciem w JS, a co nie. Chcemy tylko upewnić się, że naprawdę to rozumiemy.
Weźmy ten przykład. Mamy pętlę zliczania i chcemy wydrukować zmienną licznika na konsoli z opóźnieniem. Dlatego używamy setTimeout
i zamykania do przechwytywania wartości zmiennej licznika, aby upewnić się, że nie zostanie wydrukowana N razy wartość N.
Niewłaściwe rozwiązanie bez zamknięć lub czegokolwiek w pobliżu zamknięć to:
for(var i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
który oczywiście wydrukuje 10-krotność wartości i
po pętli, a mianowicie 10.
Więc jego próba była:
for(var i = 0; i < 10; i++) {
(function(){
var i2 = i;
setTimeout(function(){
console.log(i2);
}, 1000)
})();
}
drukowanie 0 do 9 zgodnie z oczekiwaniami.
Powiedziałem mu, że nie używa zamknięcia do schwytania i
, ale upiera się, że tak. Udowodniłem, że nie używa zamknięć , umieszczając ciało pętli for w innym setTimeout
(przekazując swoją anonimową funkcję setTimeout
), drukując ponownie 10 razy 10. To samo dotyczy, jeśli przechowuję jego funkcję w a var
i wykonuję ją po pętli, drukując również 10 razy 10. Więc moim argumentem jest to, że tak naprawdę nie przechwytuje wartościi
, czyniąc swoją wersję nie zakończeniem.
Moja próba była:
for(var i = 0; i < 10; i++) {
setTimeout((function(i2){
return function() {
console.log(i2);
}
})(i), 1000);
}
Więc wychwytywania I i
(o nazwie i2
wewnątrz zamknięcia), ale teraz wrócić inną funkcję i przekazać te okolice. W moim przypadku funkcja przekazana do setTimeout naprawdę przechwytuje i
.
Kto teraz używa zamknięć, a kto nie?
Zauważ, że oba rozwiązania drukują opóźnienie od 0 do 9 na konsoli, więc rozwiązują pierwotny problem, ale chcemy zrozumieć, które z tych dwóch rozwiązań używa zamknięć, aby to osiągnąć.
źródło
Odpowiedzi:
Uwaga edytora: Wszystkie funkcje w JavaScript są zamknięciami, jak wyjaśniono w tym poście . Interesuje nas jednak tylko identyfikacja podzbioru tych funkcji, które są interesujące z teoretycznego punktu widzenia. Odtąd wszelkie odniesienia do słowa „ zamknięcie” będą odnosić się do tego podzbioru funkcji, chyba że zaznaczono inaczej.
Proste wyjaśnienie dla zamknięć:
Teraz użyjmy tego, aby dowiedzieć się, kto używa zamknięć, a kto nie (dla wyjaśnienia nazwałem funkcje):
Przypadek 1: Program twojego przyjaciela
W powyższym programie są dwie funkcje:
f
ig
. Zobaczmy, czy są zamknięciami:Dla
f
:i2
jest zmienną lokalną .i
jest wolną zmienną.setTimeout
jest wolną zmienną.g
jest zmienną lokalną .console
jest wolną zmienną.i
jest związany z zasięgiem globalnym.setTimeout
jest związany z zasięgiem globalnym.console
jest związany z zasięgiem globalnym.i
nie jest zamknięty przezf
.setTimeout
nie jest zamknięty przezf
.console
nie jest zamknięty przezf
.Zatem funkcja
f
nie jest zamknięciem.Dla
g
:console
jest wolną zmienną.i2
jest wolną zmienną.console
jest związany z zasięgiem globalnym.i2
jest związany z zakresemf
.setTimeout
.console
nie jest zamknięty przezg
.i2
jest zamknięty przezg
.Zatem funkcja
g
jest zamknięcie dla zmiennej wolneji2
(który jest dla upvalueg
) kiedy to odwołanie od wewnątrzsetTimeout
.Źle dla ciebie: Twój przyjaciel używa zamknięcia. Wewnętrzną funkcją jest zamknięcie.
Przypadek 2: Twój program
W powyższym programie są dwie funkcje:
f
ig
. Zobaczmy, czy są zamknięciami:Dla
f
:i2
jest lokalnyzmienną .g
jest lokalnyzmienną .console
jest wolną zmienną.console
jest związany z zasięgiem globalnym.console
nie jest zamknięty przezf
.Zatem funkcja
f
nie jest zamknięciem.Dla
g
:console
jest wolną zmienną.i2
jest wolną zmienną.console
jest związany z zasięgiem globalnym.i2
jest związany z zakresemf
.setTimeout
.console
nie jest zamknięty przezg
.i2
jest zamknięty przezg
.Zatem funkcja
g
jest zamknięcie dla zmiennej wolneji2
(który jest dla upvalueg
) kiedy to odwołanie od wewnątrzsetTimeout
.Dobrze dla ciebie: używasz zamknięcia. Wewnętrzną funkcją jest zamknięcie.
Więc ty i twój przyjaciel używacie zamknięć. Nie kłóćcie się. Mam nadzieję, że wyjaśniłem pojęcie zamknięć i jak je zidentyfikować dla was obojga.
Edytować: Proste wyjaśnienie, dlaczego wszystkie funkcje są zamknięte (napisy @Peter):
Najpierw rozważmy następujący program (to kontrola ):
lexicalScope
iregularFunction
nie są zamknięciami z powyższej definicji .message
ostrzeżenia, ponieważregularFunction
nie jest to zamknięcie (tzn. Ma on dostęp do wszystkich zmiennych w zakresie nadrzędnym - w tymmessage
).message
jest to rzeczywiście zaalarmowane.Następnie rozważmy następujący program (jest to alternatywa ):
closureFunction
zamknięcie powyższej definicji .message
ostrzeżenia, ponieważclosureFunction
jest to zamknięcie (tzn. Ma on dostęp do wszystkich zmiennych nielokalnych w momencie tworzenia funkcji ( zobacz tę odpowiedź) ) - nie obejmuje tomessage
).message
jest on w rzeczywistości ostrzegany.Co z tego wywnioskujemy?
źródło
g
działa w zakresiesetTimeout
, ale w przypadku 2 mówisz, żef
działa w zakresie globalnym. Oba są w ramach setTimeout, więc jaka jest różnica?Zgodnie z
closure
definicją:Używasz,
closure
jeśli zdefiniujesz funkcję, która używa zmiennej, która jest zdefiniowana poza funkcją. (nazywamy zmienną wolną zmienną ).Wszystkie używają
closure
(nawet w pierwszym przykładzie).źródło
i2
zdefiniowanej na zewnątrz.W skrócie Javascript Zamknięcia pozwolić funkcji na dostęp do zmiennej , która jest zadeklarowana w funkcji leksykalno-dominującej .
Zobaczmy bardziej szczegółowe wyjaśnienie. Aby zrozumieć zamknięcia, ważne jest zrozumienie, w jaki sposób JavaScript mierzy zmienne.
Zakresy
W JavaScript zakresy są definiowane za pomocą funkcji. Każda funkcja określa nowy zakres.
Rozważ następujący przykład;
wywoływanie f wydruków
Rozważmy teraz przypadek, w którym mamy funkcję
g
zdefiniowaną w innej funkcjif
.Zadzwonimy
f
do leksykalnego rodzica zg
. Jak wyjaśniono wcześniej, mamy teraz 2 zakresy; zakresf
i zakresg
.Ale jeden zakres jest „w obrębie” drugiego, więc czy zakres funkcji potomnej jest częścią zakresu funkcji rodzica? Co dzieje się ze zmiennymi zadeklarowanymi w zakresie funkcji nadrzędnej; czy będę mógł uzyskać do nich dostęp z zakresu funkcji potomnej? Właśnie tam wkraczają zamknięcia.
Domknięcia
W JavaScript funkcja
g
może nie tylko uzyskać dostęp do dowolnych zmiennych zadeklarowanych w zakresie,g
ale także uzyskać dostęp do dowolnych zmiennych zadeklarowanych w zakresie funkcji nadrzędnejf
.Rozważ następujące;
wywoływanie f wydruków
Spójrzmy na linię
console.log(foo);
. W tym momencie jesteśmy w zakresieg
i próbujemy uzyskać dostęp do zmiennejfoo
zadeklarowanej w zakresief
. Ale jak wspomniano wcześniej, możemy uzyskać dostęp do dowolnej zmiennej zadeklarowanej w leksykalnej funkcji nadrzędnej, co ma miejsce tutaj;g
jest leksykalnym rodzicemf
. Dlategohello
jest drukowany.Spójrzmy teraz na linię
console.log(bar);
. W tym momencie jesteśmy w zakresief
i próbujemy uzyskać dostęp do zmiennejbar
zadeklarowanej w zakresieg
.bar
nie jest zadeklarowany w bieżącym zakresie, a funkcjag
nie jest elementem nadrzędnymf
, dlategobar
jest niezdefiniowanaW rzeczywistości możemy również uzyskać dostęp do zmiennych zadeklarowanych w zakresie leksykalnej funkcji „dziadka”. Dlatego jeśli w funkcji będzie
h
zdefiniowana funkcjag
wtedy
h
będzie w stanie uzyskać dostęp do wszystkich zmiennych zadeklarowanych w zakresie funkcjih
,g
orazf
. Odbywa się to przy zamknięciach . W JavaScript zamknięcia pozwalają nam na dostęp do dowolnej zmiennej zadeklarowanej w leksykalnej funkcji nadrzędnej, w leksykalnej funkcji grand-grand, w leksykalnej funkcji grand-grand, itp. Można to postrzegać jako łańcuch zasięgu ;scope of current function -> scope of lexical parent function -> scope of lexical grand parent function -> ...
do ostatniej funkcji nadrzędnej, która nie ma leksykalnego rodzica.Obiekt okna
W rzeczywistości łańcuch nie kończy się na ostatniej funkcji nadrzędnej. Jest jeszcze jeden specjalny zakres; zakres globalny . Każda zmienna niezadeklarowana w funkcji jest uważana za zadeklarowaną w zasięgu globalnym. Globalny zasięg ma dwie specjalności;
window
obiektu.Dlatego istnieją dokładnie dwa sposoby deklarowania zmiennej
foo
w zasięgu globalnym; albo przez nie deklarowanie go w funkcji, ani przez ustawienie właściwościfoo
obiektu window.Obie próby wykorzystują zamknięcia
Po przeczytaniu bardziej szczegółowego wyjaśnienia może się teraz okazać, że oba rozwiązania wykorzystują zamknięcia. Ale dla pewności zróbmy dowód.
Stwórzmy nowy język programowania; JavaScript bez zamykania. Jak sama nazwa wskazuje, JavaScript-No-Closure jest identyczny jak JavaScript, z tym wyjątkiem, że nie obsługuje Zamknięć.
Innymi słowy;
Dobra, zobaczmy, co się stanie z pierwszym rozwiązaniem z JavaScript-No-Closure;
dlatego zostanie wydrukowany
undefined
10 razy w JavaScript-No-Closure.Dlatego pierwsze rozwiązanie wykorzystuje zamknięcie.
Spójrzmy na drugie rozwiązanie;
dlatego zostanie wydrukowany
undefined
10 razy w JavaScript-No-Closure.Oba rozwiązania wykorzystują zamknięcia.
Edycja: Zakłada się, że te 3 fragmenty kodu nie są zdefiniowane w zakresie globalnym. W przeciwnym razie zmienne
foo
ii
byłyby powiązane zwindow
obiektem, a zatem są dostępne przezwindow
obiekt zarówno w JavaScript, jak i JavaScript-No-Closure.źródło
i
można zdefiniować? Po prostu odwołujesz się do zakresu nadrzędnego, który jest nadal ważny, jeśli nie było żadnych zamknięć.i2
toi
w momencie definiowania swojej funkcji. To czynii
NIE zmienną darmową. Nadal uważamy twoją funkcję za zamknięcie, ale bez żadnej zmiennej swobodnej to jest sedno.Nigdy nie byłem zadowolony ze sposobu, w jaki ktoś to wyjaśnia.
Kluczem do zrozumienia zamknięć jest zrozumienie, jaki byłby JS bez zamknięć.
Bez zamknięć spowodowałoby to błąd
Gdy outerFunc powróci w wyimaginowanej wersji JavaScript z wyłączoną funkcją zamykania, odwołanie do outerVar zostanie wyrzucone na śmietnik i zniknie, pozostawiając nic dla odniesienia do wewnętrznego func.
Zamknięcia są zasadniczo specjalnymi regułami, które uruchamiają się i umożliwiają istnienie tych wariacji, gdy funkcja wewnętrzna odwołuje się do zmiennych funkcji zewnętrznej. W przypadku zamknięć wspomniane zmienne są zachowywane nawet po wykonaniu funkcji zewnętrznej lub „zamknięciu”, jeśli pomaga to zapamiętać punkt.
Nawet przy zamknięciach cykl życia lokalnych zmiennych w funkcji bez wewnętrznych funkcji, które odwołują się do miejscowych, działa tak samo, jak w wersji bez zamknięcia. Po zakończeniu funkcji miejscowi otrzymują śmieci.
Kiedy już masz odniesienie w funkcji wewnętrznej do zewnętrznego var, jest to jednak tak, jakby framuga drzwi przeszkadzała w wyrzucaniu elementów bezużytecznych dla wspomnianych zmiennych.
Być może bardziej dokładnym sposobem spojrzenia na zamknięcia jest to, że funkcja wewnętrzna zasadniczo wykorzystuje zakres wewnętrzny jako własną funkcję zakresu.
Ale kontekst, do którego się odwołuje, jest w rzeczywistości trwały, a nie jak migawka. Wielokrotne uruchamianie zwróconej funkcji wewnętrznej, która stale zwiększa i rejestruje lokalny var funkcji zewnętrznej, będzie ostrzegać o wyższych wartościach.
źródło
Oboje używacie zamknięć.
Idę z definicją Wikipedii tutaj:
Próba twojego znajomego wyraźnie wykorzystuje zmienną
i
, która jest nielokalna, biorąc jej wartość i wykonując kopię do zapisania w lokalneji2
.Twoja własna próba
i
jest przekazywana jako argument do anonimowej funkcji (która znajduje się w witrynie wywoływania). Jak dotąd nie jest to zamknięcie, ale wtedy ta funkcja zwraca inną funkcję, która odwołuje się do tego samegoi2
. Ponieważ wewnętrzna funkcja anonimowai2
nie jest lokalna, powoduje to zamknięcie.źródło
i
doi2
, a następnie definiuje logikę i wykonuje tę funkcję. Gdybym nie wykonał go natychmiast, ale zapisałbym go w var i wykonał po pętli, wypisałby 10, prawda? Więc nie uchwycił ja.i
dobrze zrobione . Zachowanie, które opisujesz, nie jest wynikiem zamknięcia a nie zamknięcia; jest to wynik zmiany w międzyczasie zmiennej zamkniętej. Robisz to samo, używając innej składni, natychmiast wywołując funkcję i przekazująci
jako argument (który natychmiast kopiuje jej bieżącą wartość). Jeśli umieścisz własnysetTimeout
w innym, stanie sięsetTimeout
to samo.Ty i twój przyjaciel używacie zamknięć:
W kodzie twojego przyjaciela funkcja
function(){ console.log(i2); }
zdefiniowana w zamknięciu funkcji anonimowejfunction(){ var i2 = i; ...
i może odczytywać / zapisywać zmienną lokalnąi2
.W twoim kodzie funkcja
function(){ console.log(i2); }
zdefiniowana w zamknięciu funkcjifunction(i2){ return ...
i może odczytywać / zapisywać wartości lokalnei2
(zadeklarowane w tym przypadku jako parametr).W obu przypadkach funkcja
function(){ console.log(i2); }
przechodzi dosetTimeout
.Innym równoważnym (ale przy mniejszym zużyciu pamięci) jest:
źródło
Zamknięcie
Zamknięcie nie jest funkcją ani wyrażeniem. Musi być postrzegany jako rodzaj „migawki” ze zmiennych używanych poza zakresem funkcji i używanych wewnątrz funkcji. Gramatycznie należy powiedzieć: „zamknij zmienne”.
Innymi słowy: zamknięcie jest kopią odpowiedniego kontekstu zmiennych, od których zależy funkcja.
Jeszcze raz (naïf): Zamknięcie ma dostęp do zmiennych, które nie są przekazywane jako parametr.
Pamiętaj, że te koncepcje funkcjonalne silnie zależą od używanego języka programowania / środowiska. W JavaScript zamknięcie zależy od zakresu leksykalnego (co jest prawdą w większości języków c).
Tak więc, zwracanie funkcji polega głównie na zwróceniu funkcji anonimowej / nienazwanej. Gdy zmienna dostępu do funkcji, nieprzekazana jako parametr, i w jej (leksykalnym) zakresie, została zamknięta.
A więc jeśli chodzi o twoje przykłady:
Wszyscy używają zamknięć. Nie myl punktu wykonania z zamknięciami. Jeśli „migawka” zamknięć zostanie wykonana w niewłaściwym momencie, wartości mogą być nieoczekiwane, ale na pewno zostanie wykonane zamknięcie!
źródło
Spójrzmy na oba sposoby:
Deklaruje i natychmiast wykonuje anonimową funkcję działającą
setTimeout()
we własnym kontekście. Bieżąca wartośći
jest zachowywana przez utworzenie kopii wi2
pierwszej; działa z powodu natychmiastowego wykonania.Deklaruje kontekst wykonania funkcji wewnętrznej, w której
i
zachowana jest bieżąca wartość parametrui2
; to podejście wykorzystuje również natychmiastowe wykonanie w celu zachowania wartości.Ważny
Należy wspomnieć, że semantyka uruchamiania NIE jest taka sama między obydwoma podejściami; twoja funkcja wewnętrzna zostaje przekazana,
setTimeout()
podczas gdy jego funkcja wewnętrzna nazywa sięsetTimeout()
sobą.Zawinięcie obu kodów w innym
setTimeout()
nie świadczy o tym, że tylko drugie podejście wykorzystuje zamknięcia, po prostu nie jest to samo na początek.Wniosek
Obie metody wykorzystują zamknięcia, więc sprowadza się to do osobistego gustu; drugie podejście jest łatwiejsze do „przemieszczenia się” lub uogólnienia.
źródło
setTimeout()
?i
może być zmieniana bez wpływu na to, co funkcja ma wydrukować, niezależnie od tego, gdzie i kiedy ją wykonamy.()
, przekazując w ten sposób funkcję, a zobaczysz 10 razy wynik10
.()
właśnie to sprawia, że jego kod działa, podobnie jak twój(i)
; nie tylko otoczyłeś jego kod, wprowadziłeś do niego zmiany ... dlatego nie możesz już dokonywać prawidłowego porównania.Napisałem to jakiś czas temu, aby przypomnieć sobie, czym jest zamknięcie i jak działa w JS.
Zamknięcie to funkcja, która po wywołaniu używa zakresu, w którym została zadeklarowana, a nie zakresu, w którym została wywołana. W javaScript wszystkie funkcje zachowują się w ten sposób. Zmienne wartości w zakresie pozostają tak długo, jak długo istnieje funkcja, która na nie wskazuje. Wyjątkiem od reguły jest „to”, które odnosi się do obiektu, w którym znajduje się funkcja, gdy zostanie wywołana.
źródło
Po dokładnym zbadaniu wygląda na to, że oboje korzystacie z zamknięcia.
W twoim przypadku
i
jest dostępny w anonimowej funkcji 1 ii2
jest dostępny w anonimowej funkcji 2, gdzieconsole.log
jest obecny.W twoim przypadku uzyskujesz dostęp
i2
do anonimowej funkcji, jeśliconsole.log
jest obecna. Dodajdebugger;
instrukcję przedconsole.log
i w narzędziach dla programistów chrome pod „Zakres zmiennych” powie, pod jakim zakresem jest zmienna.źródło
Rozważ następujące. To tworzy i odtwarza funkcję,
f
która się zamykai
, ale różne !:podczas gdy poniższy zamyka sam „funkcję”
(same! fragment po tym używa pojedynczego odwołania
f
)lub ściślej:
NB ostatnia definicja
f
Isfunction(){ console.log(9) }
zanim0
zostanie wydrukowany.Zastrzeżenie! Koncepcja zamknięcia może być przymusowym odwróceniem uwagi od istoty programowania elementarnego:
x-refs .:
Jak działają zamknięcia JavaScript?
Zamknięcia JavaScript Wyjaśnienie
Czy zamknięcie (JS) wymaga funkcji wewnątrz funkcji
Jak rozumieć zamknięcia w JavaScript?
JavaScript zamieszanie zmiennych lokalnych i globalnych
źródło
Run' only was desired - not sure how to remove the
KopiujChciałbym podzielić się moim przykładem i wyjaśnieniem na temat zamknięć. Zrobiłem przykład w Pythonie i dwie cyfry, aby zademonstrować stany.
Dane wyjściowe tego kodu byłyby następujące:
Oto dwie figury przedstawiające stosy i zamknięcie przymocowane do obiektu funkcyjnego.
gdy funkcja jest zwracana z programu maker
gdy funkcja zostanie wywołana później
Gdy funkcja jest wywoływana przez parametr lub zmienną nielokalną, kod potrzebuje lokalnych powiązań zmiennych, takich jak margin_top, padding oraz a, b, n. Aby zapewnić funkcjonowanie kodu funkcji, ramka stosu funkcji producenta, która już dawno zniknęła, powinna być dostępna, co jest podtrzymywane w zamknięciu, które możemy znaleźć wraz z obiektem komunikatu funkcji.
źródło
delete
link pod odpowiedzią.