Dla osób rozpoczynających swoją przygodę z językiem JavaScript jedno z trudniejszych zagadnień stanowią domknięcia. W tym artykule postaram się przybliżyć ten ciekawy temat odpowiadając na pytania czym są domknięcia oraz przedstawiając przykłady ich użycia w kodzie.
Definicji domknięć w JavaScript jest wiele. Głównie sprowadzają się do tego, że jest to wydzielony obszar stworzony przez główną funkcję, w której wszystkie zmienne i wewnętrzne funkcje są niezależne od pozostałej części kodu. Zaletą domknięć jest to, że wewnętrzne funkcje mają dostęp zewnętrznych funkcji, natomiast zewnętrzne funkcje nie mają dostępu do wewnętrznych.
Temat z definicji dosyć prosty ale wciąż nie do końca rozumiany.
Najlepiej zobrazuje to prosty przykład:
var myName = ‘Marcin’; var myAge = 31; function changeData(){ var myName = ‘Piotrek’; myAge = 32; }
changeData(); console.log(myName); console.log(myAge);
W powyższym wypadku do konsoli wydrukujemy odpowiednio Marcin i 32. Zmienna myName wewnątrz funkcji changeData jest zmienną do której dostęp ma tylko funkcja myName.
Przeanalizujmy inny przykład:
function buildName(name){ var greeting = “Hello, ” + name; return greeting; }
Funkcja buildName() deklaruje zmienną lokalną greeting i zwraca ją. Każde wywołanie funkcji tworzy nowy zakres z nową zmienną lokalną i po wykonaniu tej funkcji, nie mamy możliwości ponownego odniesienia się do tego zakresu.
Z pomocą przychodzą nam domknięcia:
function myName(name){ var greeting = "Cześć, " + name; var sayName = function(){ var welcome = greeting + " Pozdrawiam!"; console.log(welcome); }; return sayName; }
var sayMyName = myName("Marcin"); sayMyName(); // Cześć, Marcin Pozdrawiam! sayMyName(); // Cześć, Marcin Pozdrawiam! sayMyName(); // Cześć, Marcin Pozdrawiam!
Funkcja sayName() z tego przykładu jest domknięciem.
Funkcja sayName() ma własny zasięg lokalny (ze zmienną welcome) i ma również dostęp do zakresu funkcji zewnętrznej. W tym przypadku zmienna greeting z funkcji myName().
Normalnie po wykonaniu funkcji zakres jest niszczony i nie ma do niego dostępu. Po wykonaniu funkcji myName() zakres nie zostanie w tym przypadku zniszczony. Funkcja sayMyName() nadal ma do niego dostęp.
Domknięcie służy jako brama między globalnym kontekstem a zewnętrznym zakresem.
Na koniec chciałbym przedstawić przykład bardzo często poruszany na rozmowach kwalifikacyjnych, a mianowicie słynne setTimeout w pętli.
for(var i = 0; i < 5; i++){ setTimeout(function(){ console.log(i); }, 300); }
Co spowoduje ten kod? Pierwszą myślą jest wydrukowanie do konsoli cyfr od 0 do 4. Jest to częsty błąd. Kod ten wydrukuje pięć razy piątkę.
Dlaczego tak się dzieje? Zmienna i jest w przy wypadku zmienną globalną i za każdym razem przekazujemy tą samą wartość zmiennej i.
Jak uzyskać wynik od 0 do 4?
Możemy skorzystać z domknięć, a dokładniej z funkcji IIFE (Immediately-Invoked Function Expression). Przykład ten będzie wyglądał tak:
for (var i = 0; i < 5; i++){ (function (e){ setTimeout(function (){ console.log(e); }, 300); })(i); }
Dlaczego teraz osiągniemy zamierzony efekt? Ponieważ funkcja anonimowa ma swój własny zakres i za każdym razem przekazujemy do niej inną wartość zmiennej i.
Przy pierwszym przebiegu pętli, wywołujemy od razu IIFE z parametrem i o aktualnej wartości 0 (wartość z domknięcia). Wywołanie IIFE powoduje ustawienie instrukcji console.log do wykonania z 300ms opóźnieniem z wartością zmiennej e równej 0. Rozpoczyna się nowe przejście przez pętlę. Tym razem zmienna i ma wartość 1 i zostaje przekazana do IIFE, która ustawia wykonanie console.log za 300ms ze zmienną e, o wartości 1 i tak aż do 4.
Podsumowując:
Domknięć możemy użyć w sytuacjach w których:
- Chcemy emulować enkapsulację metod
- Normalnie użylibyśmy obiektu z wyłącznie jedną metodą