Wezwania zwrotne, obsługa zdarzeń, funkcje wyższego rzędu mogą uzyskać dostęp do zmiennych zewnętrznego zakresu dzięki zamknięciom. Domknięcia są ważne w programowaniu funkcyjnym, i są często zadawane podczas rozmowy kwalifikacyjnej w JavaScript.
Mimo, że są używane wszędzie, domknięcia są trudne do zrozumienia. Jeśli jeszcze nie miałeś swojego momentu „Aha!” w zrozumieniu domknięć, to ten post jest dla Ciebie.
Zacznę od podstawowych pojęć: zakres i zakres leksykalny. Następnie, po ogarnięciu podstaw, będziesz potrzebował tylko jednego kroku, aby w końcu zrozumieć domknięcia.
Zanim zaczniesz, sugeruję, abyś oparł się chęci pominięcia sekcji dotyczących zakresu i zakresu leksykalnego. Te pojęcia są kluczowe dla domknięć, a jeśli je dobrze zrozumiesz, idea domknięcia stanie się oczywista.
1. Zakres
Gdy definiujesz zmienną, chcesz, aby była ona dostępna w pewnych granicach. Np. zmienna result
ma sens istnieć w ramach funkcji calculate()
, jako wewnętrzny szczegół. Poza funkcją calculate()
, zmienna result
jest bezużyteczna.
Dostępność zmiennych jest zarządzana przez zakres. Możesz swobodnie uzyskać dostęp do zmiennej zdefiniowanej w jej zakresie. Ale poza tym zakresem, zmienna jest niedostępna.
W JavaScript, zakres jest tworzony przez funkcję lub blok kodu.
Zobaczmy, jak zakres wpływa na dostępność zmiennej count
. Zmienna ta należy do zakresu utworzonego przez funkcję foo()
:
function foo() { // The function scope let count = 0; console.log(count); // logs 0}foo();console.log(count); // ReferenceError: count is not defined
count
jest swobodnie dostępna w ramach zakresu foo()
.
Jednakże poza zakresem foo()
count
jest niedostępny. Jeśli mimo to spróbujesz uzyskać dostęp do count
z zewnątrz, JavaScript rzuca ReferenceError: count is not defined
.
W JavaScript, zakres mówi: jeśli zdefiniowałeś zmienną wewnątrz funkcji lub bloku kodu, to możesz użyć tej zmiennej tylko wewnątrz tej funkcji lub bloku kodu. Powyższy przykład demonstruje to zachowanie.
Teraz zobaczmy ogólne sformułowanie:
Zakres to polityka przestrzeni, która rządzi dostępnością zmiennych.
Od razu pojawia się własność: zakres izoluje zmienne. To jest świetne, ponieważ różne zakresy mogą mieć zmienne o tej samej nazwie.
Możesz ponownie użyć wspólnych nazw zmiennych (count
index
current
value
, itp) w różnych zakresach bez kolizji.
foo()
i bar()
zakresy funkcji mają swoje własne, ale tak samo nazwane, zmienne count
:
function foo() { // "foo" function scope let count = 0; console.log(count); // logs 0}function bar() { // "bar" function scope let count = 1; console.log(count); // logs 1}foo();bar();
count
zmienne z zakresów funkcji foo()
i bar()
nie kolidują ze sobą.
2. Zagnieżdżanie zakresów
Zabawmy się jeszcze trochę z zakresami i umieśćmy jeden zakres w drugim.
Funkcja innerFunc()
jest zagnieżdżona wewnątrz zewnętrznej funkcji outerFunc()
.
Jak te 2 zakresy funkcji oddziaływałyby na siebie? Czy mogę uzyskać dostęp do zmiennej outerVar
z outerFunc()
z wewnątrz innerFunc()
zakresu?
Spróbujmy tego w przykładzie:
function outerFunc() { // the outer scope let outerVar = 'I am outside!'; function innerFunc() { // the inner scope console.log(outerVar); // => logs "I am outside!" } innerFunc();}outerFunc();
Zmienna outerVar
jest dostępna wewnątrz zakresu innerFunc()
. Zmienne zewnętrznego zakresu są dostępne wewnątrz wewnętrznego zakresu.
Teraz wiesz już 2 ciekawe rzeczy:
- Zakresy mogą być zagnieżdżone
- Zmienne zewnętrznego zakresu są dostępne wewnątrz wewnętrznego zakresu
3. Zakres leksykalny
Jak JavaScript rozumie, że outerVar
wewnątrz innerFunc()
odpowiada zmiennej outerVar
z outerFunc()
?
To dlatego, że JavaScript implementuje mechanizm zakresów zwany zakresem leksykalnym (lub zakresem statycznym). Skopiowanie leksykalne oznacza, że dostępność zmiennych jest określona przez pozycję zmiennych w kodzie źródłowym wewnątrz zakresów zagnieżdżania.
W uproszczeniu, zakres leksykalny oznacza, że wewnątrz wewnętrznego zakresu można uzyskać dostęp do zmiennych jego zewnętrznych zakresów.
Nazywamy to leksykalnym (lub statycznym) ponieważ silnik określa (w czasie leksykowania) zagnieżdżenie zakresów tylko poprzez spojrzenie na kod źródłowy JavaScript, bez jego wykonywania.
Oto jak silnik rozumie poprzedni wycinek kodu:
- Widzę, że definiujesz funkcję
outerFunc()
, która posiada zmiennąouterVar
. Dobrze. - Wewnątrz
outerFunc()
, widzę, że definiujesz funkcjęinnerFunc()
. - Wewnątrz
innerFunc()
, widzę zmiennąouterVar
bez deklaracji. Ponieważ używam leksykalnego zakresu, uważam, że zmiennaouterVar
wewnątrzinnerFunc()
jest tą samą zmienną, coouterVar
zouterFunc()
.
Destylowana idea zakresu leksykalnego:
Zakres leksykalny składa się z zakresów zewnętrznych określanych statycznie.
Na przykład:
const myGlobal = 0;function func() { const myVar = 1; console.log(myGlobal); // logs "0" function innerOfFunc() { const myInnerVar = 2; console.log(myVar, myGlobal); // logs "1 0" function innerOfInnerOfFunc() { console.log(myInnerVar, myVar, myGlobal); // logs "2 1 0" } innerOfInnerOfFunc(); } innerOfFunc();}func();
Zakres leksykalny innerOfInnerOfFunc()
składa się z zakresów innerOfFunc()
func()
oraz zakresu globalnego (zakres skrajny). W ramach innerOfInnerOfFunc()
można uzyskać dostęp do zmiennych zakresu leksykalnego myInnerVar
myVar
i myGlobal
.
Zakres leksykalny innerFunc()
składa się z func()
oraz zakresu globalnego. W obrębie innerOfFunc()
można uzyskać dostęp do zmiennych zakresu leksykalnego myVar
i myGlobal
.
Wreszcie, zakres leksykalny func()
składa się tylko z zakresu globalnego. Wewnątrz func()
można uzyskać dostęp do zmiennej zakresu leksykalnego myGlobal
.
4. Domknięcie
Ok, zakres leksykalny pozwala na statyczny dostęp do zmiennych zewnętrznych zakresów. Pozostał już tylko jeden krok do zamknięcia!
Przyjrzyjmy się jeszcze raz przykładowi outerFunc()
i innerFunc()
:
function outerFunc() { let outerVar = 'I am outside!'; function innerFunc() { console.log(outerVar); // => logs "I am outside!" } innerFunc();}outerFunc();
Wewnątrz zakresu innerFunc()
dostęp do zmiennej outerVar
uzyskujemy z zakresu leksykalnego. To już wiemy.
Zauważmy, że wywołanie innerFunc()
dzieje się wewnątrz jego zakresu leksykalnego (zakres outerFunc()
).
Zróbmy zmianę: innerFunc()
na wywołanie poza jego zakresem leksykalnym (poza outerFunc()
). Czy innerFunc()
nadal byłby w stanie uzyskać dostęp do outerVar
?
Zróbmy poprawki w snippecie kodu:
function outerFunc() { let outerVar = 'I am outside!'; function innerFunc() { console.log(outerVar); // => logs "I am outside!" } return innerFunc;}const myInnerFunc = outerFunc();myInnerFunc();
Teraz innerFunc()
jest wykonywany poza swoim zakresem leksykalnym. I co ważne:
innerFunc()
nadal ma dostęp do outerVar
ze swojego zakresu leksykalnego, nawet będąc wykonywanym poza swoim zakresem leksykalnym.
Innymi słowy, innerFunc()
zamyka nad (a.k.a. przechwytuje, zapamiętuje) zmienną outerVar
ze swojego zakresu leksykalnego.
Innymi słowy, innerFunc()
jest zamknięciem, ponieważ zamyka nad zmienną outerVar
z jej zakresu leksykalnego.
Wykonałeś ostatni krok do zrozumienia, czym jest zamknięcie:
Zamknięcie jest funkcją, która uzyskuje dostęp do swojego zakresu leksykalnego nawet wykonywana poza swoim zakresem leksykalnym.
W uproszczeniu, domknięcie to funkcja, która zapamiętuje zmienne z miejsca, w którym została zdefiniowana, niezależnie od tego, gdzie jest wykonywana później.
Zasada identyfikacji zamknięcia: jeśli widzisz w funkcji obcą zmienną (niezdefiniowaną wewnątrz funkcji), najprawdopodobniej ta funkcja jest zamknięciem, ponieważ obca zmienna jest przechwytywana.
W poprzednim fragmencie kodu, outerVar
jest obcą zmienną wewnątrz domknięcia innerFunc()
przechwyconą z outerFunc()
zakresu.
Kontynuujmy z przykładami, które pokazują, dlaczego zamknięcie jest przydatne.
5. Przykłady domknięć
5.1 Obsługa zdarzeń
Wyświetlmy ile razy został kliknięty przycisk:
let countClicked = 0;myButton.addEventListener('click', function handleClick() { countClicked++; myText.innerText = `You clicked ${countClicked} times`;});
Otwórz demo i kliknij przycisk. Tekst aktualizuje się, pokazując liczbę kliknięć.
Kiedy przycisk zostanie kliknięty, handleClick()
jest wykonywany gdzieś wewnątrz kodu DOM. Wykonanie to dzieje się daleko od miejsca definicji.
Ale będąc closure, handleClick()
przechwytuje countClicked
z zakresu leksykalnego i aktualizuje go, gdy nastąpi kliknięcie. Co więcej, myText
jest również przechwytywany.
5.2 Wywołania zwrotne
Przechwytywanie zmiennych z zakresu leksykalnego jest przydatne w wywołaniach zwrotnych.
A setTimeout()
callback:
const message = 'Hello, World!';setTimeout(function callback() { console.log(message); // logs "Hello, World!"}, 1000);
callback()
jest domknięciem, ponieważ przechwytuje zmienną message
.
Funkcja iteratora dla forEach()
:
let countEven = 0;const items = ;items.forEach(function iterator(number) { if (number % 2 === 0) { countEven++; }});countEven; // => 2
Funkcja iterator
jest domknięciem, ponieważ przechwytuje zmienną countEven
.
5.3 Programowanie funkcyjne
Currying ma miejsce, gdy funkcja zwraca inną funkcję, dopóki argumenty nie zostaną w pełni dostarczone.
Na przykład:
function multiply(a) { return function executeMultiply(b) { return a * b; }}const double = multiply(2);double(3); // => 6double(5); // => 10const triple = multiply(3);triple(4); // => 12
multiply
jest funkcją curried, która zwraca inną funkcję.
Currieding, ważna koncepcja programowania funkcyjnego, jest również możliwy dzięki domknięciom.
executeMultiply(b)
to domknięcie, które przechwytuje a
ze swojego zakresu leksykalnego. Kiedy zamknięcie jest wywoływane, przechwycona zmienna a
i parametr b
są używane do obliczenia a * b
.
6. Podsumowanie
Zakres jest tym, co rządzi dostępnością zmiennych w JavaScript. Może być zakres funkcji lub zakres bloku.
Zakres leksykalny pozwala zakresowi funkcji na statyczny dostęp do zmiennych z zewnętrznych zakresów.
Finally, zamknięcie jest funkcją, która przechwytuje zmienne z jej zakresu leksykalnego. Mówiąc prościej, domknięcie zapamiętuje zmienne z miejsca, w którym zostało zdefiniowane, bez względu na to, gdzie jest wykonywane.
Closures przechwytują zmienne wewnątrz obsługi zdarzeń, callbacków. Są one używane w programowaniu funkcyjnym. Co więcej, możesz zostać zapytany o to, jak działają domknięcia podczas rozmowy kwalifikacyjnej na stanowisko Frontend.
Każdy programista JavaScript musi wiedzieć, jak działają domknięcia. Poradzić sobie z tym ⌐■■.
Co z wyzwaniem? 7 pytań na rozmowie kwalifikacyjnej dotyczących domknięć w JavaScript. Czy potrafisz na nie odpowiedzieć?