Zamknięcia w JavaScript są jednym z tych pojęć, które wielu z trudem mieści w głowie. W poniższym artykule wyjaśnię w jasny sposób, czym jest zamknięcie, i wyjaśnię to na prostych przykładach.
Zamknięcie jest cechą w JavaScript, gdzie funkcja wewnętrzna ma dostęp do zmiennych funkcji zewnętrznej (zamykającej) – łańcuch zakresu.
Zamknięcie ma trzy łańcuchy zakresów:
- ma dostęp do własnego zakresu – zmiennych zdefiniowanych pomiędzy nawiasami klamrowymi
- ma dostęp do zmiennych funkcji zewnętrznej
- ma dostęp do zmiennych globalnych
Dla niewtajemniczonych, ta definicja może wydawać się po prostu mnóstwem żargonu!
Ale czym tak naprawdę jest zamknięcie?
Przyjrzyjrzyjmy się prostemu przykładowi zamknięcia w JavaScript:
function outer() { var b = 10;
function inner() {
var a = 20;
console.log(a+b);
}
return inner;
}
Mamy tutaj dwie funkcje:
- funkcję zewnętrzną
outer
, która posiada zmiennąb
, i zwraca funkcjęinner
function - an internal function
inner
, która posiada zmiennąa
, i uzyskuje dostęp do zmiennejouter
b
, w ramach swojego ciała funkcji
Zakres zmiennej b
jest ograniczony do funkcji outer
, a zakres zmiennej a
jest ograniczony do funkcji inner
.
Wywołajmy teraz funkcję outer()
, a wynik działania funkcji outer()
zapiszmy w zmiennej X
. Następnie wywołajmy funkcję outer()
po raz drugi i zapiszmy ją w zmiennej Y
.
Zobaczmy krok po kroku, co się stanie, gdy funkcja outer()
zostanie wywołana po raz pierwszy:
- Zmienna
b
zostaje utworzona, jej zakres zostaje ograniczony do funkcjiouter()
, a jej wartość zostaje ustawiona na10
. - Kolejna linia to deklaracja funkcji, więc nic do wykonania.
- W ostatniej linii,
return inner
szuka zmiennej o nazwieinner
, stwierdza, że ta zmiennainner
jest w rzeczywistości funkcją, a więc zwraca całe ciało funkcjiinner
. - Zawartość zwracana przez instrukcję return jest przechowywana w
X
.
Tak więc,X
będzie przechowywał następujące dane:function inner() {
var a=20;
console.log(a+b);
} - Funkcja
outer()
kończy wykonywanie, a wszystkie zmienne w zakresieouter()
teraz już nie istnieją.
Ta ostatnia część jest ważna do zrozumienia. Gdy funkcja zakończy swoje wykonywanie, wszelkie zmienne, które zostały zdefiniowane wewnątrz zakresu funkcji przestają istnieć.
Czas życia zmiennej zdefiniowanej wewnątrz funkcji jest czasem życia wykonania funkcji.
Co to oznacza, że w console.log(a+b)
, zmienna b
istnieje tylko podczas wykonywania funkcji outer()
. Gdy funkcja outer
zakończy wykonywanie, zmienna b
już nie istnieje.
Gdy funkcja jest wykonywana po raz drugi, zmienne funkcji są tworzone ponownie i żyją tylko do momentu zakończenia wykonywania funkcji.
Tak więc, gdy outer()
jest wywoływany po raz drugi:
- Tworzona jest nowa zmienna
b
, jej zakres jest ograniczony do funkcjiouter()
, a jej wartość jest ustawiana na10
. - Kolejna linia to deklaracja funkcji, więc nic do wykonania.
-
return inner
zwraca całe ciało funkcjiinner
. - Zawartość zwracana przez instrukcję return jest przechowywana w
Y
. - Funkcja
outer()
kończy wykonywanie, a wszystkie zmienne w zakresieouter()
teraz już nie istnieją.
Ważnym punktem jest to, że kiedy funkcja outer()
jest wywoływana po raz drugi, zmienna b
jest tworzona na nowo. Ponadto, gdy funkcja outer()
zakończy wykonywanie po raz drugi, ta nowa zmienna b
ponownie przestaje istnieć.
To najważniejszy punkt, z którego należy zdać sobie sprawę. Zmienne wewnątrz funkcji powstają tylko wtedy, gdy funkcja jest uruchomiona, a przestają istnieć, gdy funkcja zakończy wykonywanie.
Powróćmy teraz do naszego przykładu kodu i spójrzmy na X
i Y
. Ponieważ funkcja outer()
w momencie wykonania zwraca funkcję, zmienne X
i Y
są funkcjami.
Można to łatwo sprawdzić, dodając do kodu JavaScript następujące elementy:
console.log(typeof(X)); //X is of type function
console.log(typeof(Y)); //Y is of type function
Ponieważ zmienne X
i Y
są funkcjami, możemy je wykonać. W języku JavaScript funkcję można wykonać, dodając ()
po nazwie funkcji, np. X()
i Y()
.
Kiedy wykonujemy X()
i Y()
, zasadniczo wykonujemy funkcję inner
.
Prześledźmy krok po kroku, co się dzieje, gdy X()
jest wykonywany po raz pierwszy:
- Zmienna
a
jest tworzona, a jej wartość jest ustawiana na20
. - JavaScript próbuje teraz wykonać
a + b
. Tutaj sprawy stają się interesujące. JavaScript wie, żea
istnieje, ponieważ właśnie go utworzył. Jednak zmiennab
już nie istnieje. Ponieważb
jest częścią funkcji zewnętrznej,b
istniałby tylko podczas wykonywania funkcjiouter()
. Ponieważ funkcjaouter()
zakończyła wykonywanie na długo przed tym, jak wywołaliśmyX()
, wszelkie zmienne znajdujące się w zakresie funkcjiouter
przestają istnieć, a zatem zmiennab
już nie istnieje.
Jak JavaScript sobie z tym radzi?
Zamknięcia
Funkcja inner
może uzyskać dostęp do zmiennych funkcji zamykającej dzięki zamknięciom w JavaScript. Innymi słowy, funkcja inner
zachowuje łańcuch zakresu funkcji zamykającej w czasie, gdy funkcja zamykająca została wykonana, a zatem może uzyskać dostęp do zmiennych funkcji zamykającej.
W naszym przykładzie, funkcja inner
zachowała wartość b=10
, gdy funkcja outer()
została wykonana, i nadal ją zachowuje (zamyka).
Teraz odwołuje się do swojego łańcucha zakresu i zauważa, że ma wartość zmiennej b
w swoim łańcuchu zakresu, ponieważ zamknął wartość b
w ramach zamknięcia w punkcie, w którym funkcja outer
została wykonana.
Tak więc, JavaScript zna a=20
i b=10
, i może obliczyć a+b
.
Możesz to zweryfikować, dodając następującą linię kodu do powyższego przykładu:
Otwórz element Inspect w Google Chrome i przejdź do Konsoli. Możesz rozwinąć element, aby faktycznie zobaczyć element Closure
(pokazany w przedostatniej linii poniżej). Zauważ, że wartość b=10
jest zachowana w elemencie Closure
nawet po tym, jak funkcja outer()
zakończy swoje działanie.
Powróćmy teraz do definicji domknięć, którą widzieliśmy na początku i zobaczmy, czy teraz ma ona więcej sensu.
Więc funkcja wewnętrzna ma trzy łańcuchy zakresu:
- dostęp do własnego zakresu – zmienna
a
- dostęp do zmiennych funkcji
outer
– zmiennab
, którą zawiera - dostęp do dowolnych zmiennych globalnych, które mogą być zdefiniowane
Zamknięcia w akcji
Aby podkreślić znaczenie domknięć, rozszerzmy przykład o trzy linie kodu:
Kiedy uruchomisz ten kod, zobaczysz następujące wyjście w console.log
:
a=20 b=10
a=20 b=11
a=20 b=12
a=20 b=10
Przeanalizujmy ten kod krok po kroku, aby zobaczyć co dokładnie się dzieje i zobaczyć domknięcia w akcji!
var X = outer(); // outer() invoked the first time
Funkcja outer()
jest wywoływana po raz pierwszy. Wykonywane są następujące kroki:
- Tworzona jest zmienna
b
i ustawiana na10
Tworzona jest zmiennac
, i ustawiona na100
Nazwijmy tob(first_time)
ic(first_time)
dla naszego własnego odniesienia. - Funkcja
inner
jest zwracana i przypisywana doX
W tym momencie, zmiennab
jest dołączona do łańcucha zakresu funkcjiinner
jako domknięcie zb=10
, ponieważinner
używa zmiennejb
. - Funkcja
outer
kończy wykonywanie, a wszystkie jej zmienne przestają istnieć. Zmiennac
już nie istnieje, chociaż zmiennab
istnieje jako zamknięcie wewnątrzinner
.
var Y= outer(); // outer() invoked the second time
- Zmienna
b
jest tworzona od nowa i jest ustawiana na10
Zmiennac
jest tworzona od nowa.
Zauważ, że mimo iżouter()
została wykonana raz, zanim zmienneb
ic
przestały istnieć, po zakończeniu wykonywania funkcji są one tworzone jako zupełnie nowe zmienne.
Nazwijmy jeb(second_time)
ic(second_time)
dla naszego własnego odniesienia. - Funkcja
inner
zostaje zwrócona i przypisana doY
W tym momencie zmiennab
jest dołączona do łańcucha zakresu funkcjiinner
jako zamknięcie zb(second_time)=10
, ponieważinner
używa zmiennejb
. - Funkcja
outer
kończy wykonywanie, a wszystkie jej zmienne przestają istnieć.
Zmiennac(second_time)
już nie istnieje, choć zmiennab(second_time)
istnieje jako domknięcie wewnątrzinner
.
Zobaczmy teraz, co się stanie, gdy poniższe linie kodu zostaną wykonane:
X(); // X() invoked the first time
X(); // X() invoked the second time
X(); // X() invoked the third timeY(); // Y() invoked the first time
Gdy X()
jest wywoływany po raz pierwszy,
- tworzona jest zmienna
a
, i ustawiona na20
- wartość
a=20
, wartośćb
pochodzi z wartości zamknięcia.b(first_time)
, więcb=10
- zmienne
a
ib
zwiększają się o1
-
X()
kończy wykonywanie i wszystkie jego zmienne wewnętrzne – zmiennaa
– przestają istnieć.
Jednakżeb(first_time)
został zachowany jako domknięcie, więcb(first_time)
nadal istnieje.
Gdy X()
jest wywoływany po raz drugi,
- zmienna
a
jest tworzona od nowa, i ustawiona na20
Każda poprzednia wartość zmienneja
już nie istnieje, ponieważ przestała istnieć, gdyX()
zakończył wykonywanie za pierwszym razem. - wartość
a=20
wartośćb
pobierana jest z wartości zamknięcia –b(first_time)
Zauważ również, że zwiększyliśmy wartośćb
o1
z poprzedniego wykonania, więcb=11
- zmienne
a
ib
są zwiększane o1
ponownie -
X()
kończy wykonywanie i wszystkie jego zmienne wewnętrzne – zmienna a – przestają istnieć
Jednakże,b(first_time)
jest zachowany, ponieważ zamknięcie nadal istnieje.
Gdy X()
jest wywoływany po raz trzeci,
- zmienna
a
jest tworzona od nowa, i ustawiona na20
Wcześniejsza wartość zmienneja
już nie istnieje, ponieważ przestała istnieć, gdyX()
zakończył wykonywanie za pierwszym razem. - wartość
a=20
, wartośćb
pochodzi z wartości zamknięcia –b(first_time)
Zauważ również, że w poprzednim wykonaniu zwiększyliśmy wartośćb
o1
, więcb=12
- zmienne
a
ib
są inkrementowane przez1
ponownie -
X()
kończy wykonywanie, a wszystkie jego zmienne wewnętrzne – zmiennaa
– przestają istnieć
Jednakżeb(first_time)
jest zachowana, gdyż domknięcie nadal istnieje
Gdy Y() jest wywoływane po raz pierwszy,
- zmienna
a
jest tworzona od nowa i ustawiana na20
- wartość
a=20
, wartośćb
pochodzi z wartości zamknięcia –b(second_time)
, więcb=10
- zmienne
a
ib
są inkrementowane o1
-
Y()
kończy wykonywanie, i wszystkie jego zmienne wewnętrzne – zmiennaa
– przestają istnieć
Jednakżeb(second_time)
został zachowany jako domknięcie, więcb(second_time)
nadal istnieje.
Uwagi końcowe
Zamknięcia są jednym z tych subtelnych pojęć w JavaScript, które są trudne do uchwycenia na początku. Ale kiedy już je zrozumiesz, zdasz sobie sprawę, że nie mogło być inaczej.
Mam nadzieję, że te wyjaśnienia krok po kroku pomogły Ci naprawdę zrozumieć koncepcję domknięć w JavaScript!
Inne artykuły:
- Szybki przewodnik po funkcjach „samowywołujących się” w JavaScript
- Zrozumienie zakresu funkcji vs. zakresu bloku w JavaScript
- Jak używać obietnic w JavaScript
- Jak zbudować prostą animację Sprite w JavaScript