Menu

Domknięcia w JavaScript: jak działają i dlaczego są ważne

Domknięcie (closure) to funkcja, która pamięta otaczające ją zmienne. Zobacz, jak działają domknięcia w JavaScript, na przykładach do uruchomienia i w realnych zastosowaniach.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Domknięcie to funkcja, która pamięta

Za każdym razem, gdy definiujesz funkcję w JavaScript, po cichu zachowuje ona połączenie ze zmiennymi wokół siebie. Gdy funkcja uruchomi się później, nawet w zupełnie innym miejscu, nadal widzi te zmienne. To właśnie domknięcie (closure).

Najkrótsze demo:

makeGreeter się wykonuje, zwraca funkcję wewnętrzną i kończy działanie. Można by się spodziewać, że jej zmienna lokalna name zniknie, bo funkcja już skończyła. Ale funkcja wewnętrzna nadal używa name, więc JavaScript utrzymuje ją przy życiu. greetAda pamięta "Ada". greetBoris pamięta "Boris". Dwa domknięcia, dwie osobne zapamiętane wartości.

Z zakresu leksykalnego biorą się domknięcia

Reguła stojąca za domknięciami to zakres leksykalny: funkcja widzi zmienne z miejsca, w którym została zapisana, a nie z miejsca, w którym jest wywoływana. "Leksykalny" znaczy po prostu "zależny od tego, gdzie w kodzie źródłowym się znajduje".

show wypisuje "jestem na zewnątrz", a nie "jestem wewnątrz caller". Została zapisana obok zmiennej outer najwyższego poziomu, więc to ją widzi. Wywołanie jej z miejsca, które akurat ma własne outer, nie ma znaczenia.

Domknięcia to po prostu zakres leksykalny, który przeżywa funkcję zewnętrzną. Zmienna nie znika, bo ktoś wciąż trzyma do niej referencję.

Każde wywołanie dostaje własne domknięcie

Nowe wywołanie funkcji zewnętrznej tworzy nowe zmienne, a każda funkcja wewnętrzna zwrócona z tego wywołania pamięta właśnie te zmienne. Dlatego greetAda i greetBoris z przykładu powyżej się nie gryzły.

Klasyczny przykład to licznik:

a i b mają każdy własne count. Nic poza zwróconą funkcją nie może dotknąć tych zmiennych: count jest całkowicie prywatne. To nie jest funkcja języka, którą trzeba włączyć; wynika to po prostu ze sposobu działania domknięć.

Prywatne zmienne bez klas

Ponieważ zmienne w domknięciu są dostępne tylko przez zwróconą funkcję, możesz używać domknięć do budowania małych obiektów z naprawdę prywatnym stanem:

balance nie jest właściwością zwróconego obiektu, tylko żyje w domknięciu. Jedyny sposób, żeby ją odczytać albo zmienić, to metody, które udostępniasz. Klasy z polami #private też to potrafią, ale wersja z domknięciem jest od nich starsza o dekady i nadal pojawia się w całym ekosystemie.

Klasyczna pułapka w pętli

Domknięcia najczęściej zaskakują w pętlach. Zobacz, co dzieje się z var:

Spodziewasz się 0, 1, 2. Dostajesz 3, 3, 3. Oto dlaczego: var ma zasięg funkcji, więc w całej pętli jest tylko jedno i. Wszystkie trzy domknięcia przechwyciły tę samą zmienną, a gdy się uruchamiają, pętla już się skończyła i i wynosi 3.

Zmień na let:

Teraz wypisuje 0, 1, 2. let ma zasięg blokowy: każda iteracja pętli tworzy nowe wiązanie dla i, więc każde domknięcie przechwytuje własną wartość. To najważniejszy powód, żeby wybierać let zamiast var.

Domknięcia przechwytują zmienne, nie wartości

Subtelna, ale ważna rzecz: domknięcie trzyma się zmiennej, a nie migawki jej wartości z chwili zdefiniowania funkcji.

printMessage odczytuje message w momencie uruchomienia, a nie utworzenia. Jeśli chcesz mieć migawkę, najpierw skopiuj wartość do zmiennej lokalnej, co w praktyce robi let wewnątrz pętli for.

Popularny wzorzec z praktyki: once

Oto malutkie narzędzie, które używa domknięcia, żeby funkcja wykonała się tylko raz:

called i result to prywatny stan, który żyje tak długo jak zwrócona funkcja. Żadnej globalnej flagi, żadnego dodatkowego obiektu. Ten wzorzec (mała funkcja pomocnicza, prywatny stan, domknięcie) to jedna z najbardziej przydatnych rzeczy, jakie potrafi JavaScript.

Słowo o pamięci

Domknięcie utrzymuje przechwycone zmienne przy życiu, dopóki coś wciąż odwołuje się do domknięcia. Zwykle właśnie tego chcesz, ale jeśli podepniesz domknięcie do czegoś długowiecznego (na przykład event listenera DOM albo globalnego cache) i przechwyci ono coś dużego, to garbage collector nie może tego zwolnić, dopóki domknięcie nie zniknie.

function attach() {
    const hugeData = new Array(1_000_000).fill("...");
    document.addEventListener("click", () => {
        console.log(hugeData.length);
    });
}

Dopóki listener jest podpięty, hugeData zostaje w pamięci. Usuń listener (albo nie przechwytuj tego, czego nie potrzebujesz), a referencja zniknie. Nie musisz tym drobiazgowo zarządzać: po prostu pamiętaj, że domknięcia i pamięć są ze sobą powiązane.

Co warto zapamiętać

  • Domknięcie to funkcja plus zmienne, które widziała w chwili zdefiniowania.
  • Każde wywołanie funkcji zewnętrznej tworzy nowy zestaw zmiennych dla jej wewnętrznych domknięć.
  • Domknięcia dają prywatny stan bez potrzeby używania klas.
  • W pętlach używaj let, żeby każda iteracja dostała własne wiązanie.
  • Domknięcia przechwytują samą zmienną, a nie jej wartość z chwili utworzenia.

Dalej: słowo kluczowe this

Domknięcia obsługują zmienne wokół funkcji. Kolejny element to obiekt, na którym funkcja jest wywoływana: w JavaScript kontroluje to this, które zachowuje się zupełnie inaczej niż przechwycone zmienne, które właśnie omówiliśmy.

Najczęściej zadawane pytania

Czym jest domknięcie (closure) w JavaScript?

Domknięcie to funkcja, która pamięta zmienne z zakresu, w którym została zdefiniowana, nawet gdy ten zewnętrzny zakres już zakończył działanie. Technicznie każda funkcja w JavaScript jest domknięciem: to pojęcie pojawia się zwykle wtedy, gdy funkcja zostaje zwrócona albo przekazana gdzie indziej i nadal używa zmiennych ze swojego pierwotnego zakresu.

Do czego przydają się domknięcia?

Pozwalają funkcji nosić ze sobą prywatny stan. Dostajesz dane powiązane z funkcją bez potrzeby tworzenia klasy czy zmiennej globalnej. Typowe zastosowania to liczniki, callbacki wykonywane tylko raz, funkcje pomocnicze z memoizacją i ukrywanie szczegółów implementacji za małym API.

Dlaczego domknięcia dziwnie zachowują się w pętlach z var?

var ma zasięg funkcji, więc każda iteracja współdzieli tę samą zmienną. Wszystkie domknięcia utworzone w pętli odwołują się do tej jednej zmiennej, która w chwili ich uruchomienia ma już wartość końcową. Użyj zamiast tego let: ma zasięg blokowy, więc każda iteracja dostaje własne wiązanie.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ