Zrozumienie pojęcia domknięć nie przyszło mi swego czasu łatwo. Czytanie kolejnych definicji w książkach i tutorialach podnosiło mi jedynie ciśnienie, bo rozumiałem napisane w nich słowa, ale nie potrafiłem ogarnąć, o co w tym tak naprawdę chodzi. Aż w końcu nadszedł moment "eureka!". Jeśli masz podobny problem i nie chce Ci się przewalać przez dziesiątki artykułów, zapraszam do lektury.

Czym jest domknięcie?

Na szczęście - pojęciem bardzo łatwym do zrozumienia. Gorzej jednak z wyczuciem, kiedy moglibyśmy chcieć go użyć i w jakim celu. Zanim jednak przejdziemy do przykładów użycia, przydałoby się poznać definicję. W najprostszych słowach, mógłbym opisać to tak:

Domknięcie to zasięg (obszar) stworzony przez funkcję, który odgradza zgromadzone w nim zmienne i funkcje od reszty kodu tworząc dla nich osobne "środowisko"

Tak więc definicja jest - jak widzicie - banalna. Domknięcie jest wtedy, gdy postawimy płot wokół pastwiska zamykając wewnątrz wszystkie owce, wilki i ogniste smoki, aby nie pomieszały się ze stworkami z innych pastwisk. No świetnie, tylko co z tego? Zaraz do tego dojdziemy. Na razie przykład:

var a = "outside of a closure";
function myClosure() {
    var a = "inside a closure";
}
myClosure();

Tak więc zdefiniowaliśmy zmienną a i ustaliliśmy jej wartość na "outside of a closure". Następnie wywołaliśmy funkcję, która zmienia wartość zmiennej a na "inside closure", tak? Nie :) Zmienna a na zewnątrz funkcji myClosure to zupełnie inna zmienna niż zmienna a wewnątrz funkcji myClosure. Funkcja ta służy nam tutaj właśnie jako domknięcie, tworząc osobne "środowisko" dla wszystkich zmiennych w niej zawartych.

A co, gdy wewnątrz funkcji chcemy uzyskać dostęp do zmiennej, która jest na zewnątrz funkcji? Nic prostszego. Będąc wewnątrz domknięcia mamy dostęp do wszystkich zmiennych znajdujących się na zewnątrz (byle nie znajdowały się w swoim własnym domknięciu). Wystarczy pozbyć się słówka kluczowego var:

var x = 100;
var y = 200;

function myClosure() {
    var x = 500; //nie ma wpływu na zmienną globalną x
    y = 800;
}
myClosure();

console.log(x); // 100
console.log(y); // 800 - zmienna globalna została zmodyfikowana przez funkcję

OK, najprostsze mamy za sobą. Teraz przechodzimy do konkretów.

Tworzenie zmiennych publicznych i prywatnych

Jeśli programowaliście kiedyś w C++, Javie, czy podobnym języku, zauważyliście już pewnie, że JavaScript, w przeciwieństwie do nich, nie ma wbudowanej obsługi zmiennych prywatnych i publicznych. To prawda. Ale dzięki domknięciom możemy uzyskać bardzo podobny efekt.

function sheep(name) {
    var sheepName = name; //zmienna prywatna

    var privateSheepName = function () { //funkcja prywatna
        console.log(sheepName);
    };

    return { //zwracamy obiekt publiczny
        publicName: sheepName, //zmienna publiczna
        
        publicSheepName: function () { //funkcja publiczna
            console.log(sheepName)
        }
    }
}

var mySheep = sheep("Dolly"); //tworzymy obiekt

console.log(mySheep.sheepName); //undefined - obiekt bez dostępnej z zewnątrz zmiennej sheepName
mySheep.privateSheepName(); //takie wywołanie spowoduje error,
//bo obiekt nie ma dostępnej z zewnątrz metody privateSheepName

console.log(mySheep.publicName); //"Dolly"
mySheep.publicSheepName(); //"Dolly"

Symulowanie zmiennych publicznych i prywatnych jest więc w JavaScript jak najbardziej możliwe. I wykorzystujemy do tego domknięcia. Do czego więc jeszcze mogą nam się przydać?

Domknięcia w pętlach

Na początek mała zagadka. Przeanalizuj poniższy kod i powiedz, jaki będzie rezultat jego działania:

for(var i = 0; i < 5; i++) {
    setTimeout(function() {
        console.log(i);
    }, 500);
}

Jeśli myślisz, że kod ten spowoduje wypisanie kolejnych liczb 0 do 4, to... myślisz tak, jak większość, którzy zaczynali naukę tego języka - czyli źle :)

Wypisze 5 razy liczbę 5.

Jakże to waćpanie? - ze słusznym oburzeniem zapytasz. Otóż w momencie, gdy funkcja setTimeout pozwoli (po upływie pół sekundy) wywołać instrukcję console.log(i), pętla for zakończy już swoje działanie ustawiając wartość zmiennej i na 5. I przy każdym wywołaniu tej komendy i będzie miało już tę właśnie wartość.

Mała dygresja na temat sposobu działania funkcji setTimeout: Pamiętaj, że funkcja setTimeout nie zatrzymuje wywoływania całego kodu na wskazany czas, a jedynie instrukcji w niej zawartych, dlatego liczby nie będą wypisywane co pół sekundy po jednej, ale po pół sekundy od razu jedna za drugą. Dla zobrazowania: Załóżmy, że jedno przejście pętli for zajmuje komputerowi 1 ms. W pierwszym przejściu widzi funkcję setTimeout i ustawia jej wykonanie za 500 ms, czyli w 501-szej milisekundzie. W drugiej milisekundzie wchodzi do pętli drugi raz, znowu napotyka setTimeout i ustawia jego wykonanie za 500 ms, czyli w 502-giej milisekundzie działania programu itd... A po przejściu całej pętli, czyli po pięciu milisekundach, wartość zmiennej i będzie już wynosiła 5 i w takim stanie poczeka na wywołania odłożone w czasie. Oczywiście jest to duże uproszczenie i nie do końca tak to działa, jednak na potrzeby zrozumienia tematu domknięć, powinno wystarczyć. Jeśli chcesz dowiedzieć się więcej o stosie wywołań (call stack), polecam fantastyczny wykład Philipa Robertsa dostępny pod tym adresem (język angielski).

Wracając do naszego problemu. Jak więc przerobić tę pętlę tak, by wypisała oczekiwane przez nas liczby od 0 do 4?

Wykorzystać domknięcia oczywiście! Wystarczy funkcję setTimeout otoczyć domknięciem, a konkretnie funkcją anonimową, której parametrem będzie zmienna i. Mniej więcej tak:

for (var i = 0; i < 5; i++) {
    (function (e) {
        setTimeout(function () {
            console.log(e);
        }, 500);
    })(i);
}

Dzięki temu każde wywołanie funkcji setTimeout odbędzie się w osobnym domknięciu z przypisaną do niego konkretną wartością parametru. Jeśli masz problem ze zrozumieniem tego kodu, spieszę z pomocą. Rzeczą, której można tutaj nie rozumieć jest tak zwane IIFE, czyli natychmiastowe wywołanie funkcji anonimowej. Na czym to polega?

function funkcja() {
    console.log("funkcja zwkła/nazwana");
}
funkcja(); // wywołanie funkcji

function () {
    console.log("funkcja anonimowa");
} //napisana w ten sposób wywoła SyntaxError, chyba że zostanie umieszczona jako parametr innej funkcji - na przykład map lub filter

(function () {
    console.log("IIFE - funkcja anonimowa wywoływana natychmiast, gdy zostanie odczytana")
})();

(function (x) { //funkcja przyjmuje parametr i zapisuje go w zmiennej "x"
    console.log(x); //wypisze w konsoli "IIFE z parametrem"
})("IIFE z parametrem"); //przekazanie stringa jako parametr funkcji

A więc IIFE tworzymy przez otoczenie funkcji anonimowej nawiasami i dopisanie na końcu (). Dzięki temu zostanie wywołana, gdy tylko parser kodu na nią natrafi. Dodatkowo do IIFE możemy - jak do każdej funkcji - przekazać parametry wewnątrz nawiasów wywołujących funkcję - co widać na ostatnim przykładzie.

Jak więc można przeanalizować teraz nasz kod z funkcją setTimeout?. Dla przypomnienia wygląda ona tak:

for (var i = 0; i < 5; i++) {
    (function (e) {
        setTimeout(function () {
            console.log(e);
        }, 500);
    })(i);
}

Rozpoczyna się pętla for, wchodzimy do niej i od razu natrafiamy na funkcję typu IIFE, która zostaje wywołana z parametrem i o aktualnej wartości 0, zamkniętym w domknięciu. Wywołanie IIFE powoduje ustawienie instrukcji console.log do wykonania za 500 ms, z tym, że wartością zmiennej e jest w tej chwili 0 - czyli dokładnie tak, jak chcieliśmy. OK, co dalej? Rozpoczyna się nowe przejście przez pętlę. Zmienna i, która ma teraz wartość 1 zostaje przekazana do IIFE, która ustawia wykonanie console.log za 500 ms ze zmienną e o wartości 1. I tak dalej, aż do wartości 4.

Wykorzystanie domknięć w ten sposób jest szczególnie przydatne, gdy chcemy w jednej pętli przypisać zdarzenia do kilku elementów z drzewa DOM. Załóżmy, że mamy dokument HTML, w którym znajdują się poniższe elementy:

<div class="car">Good</div>
<div class="car">Better</div>
<div class="car">The best</div>

Spróbuj przeanalizować poniższy kod:

var cars = ["Porsche", "Ferrari", "Lamborghini"];
var buttons = document.getElementsByClassName("car");

for (var i = 0; i < buttons.length; i++) {
    buttons[i].onclick = function(){
        console.log(cars[i]);
    }
}

W tym momencie powinieneś już dobrze wiedzieć, że klikając na poszczególne DIVy, konsola będzie logowała wartość undefined. Dlaczego? Gdyż - tak, jak w przypadku przykładu z setTimeout - w momencie, gdy klikasz na dany element, pętla już dawno zakończyła swoje działanie i ustawiła wartość i na 3. A elementu o takim indeksie w tablicy nie ma.

Co więc zrobić, by to naprawić? Tak, dobrze myślisz: wykorzystać domknięcia.

var cars = ["Porsche", "Ferrari", "Lamborghini"];
var buttons = document.getElementsByClassName("car");

for (var i = 0; i < buttons.length; i++) {
    (function(e) {
        buttons[i].onclick = function () {
            console.log(cars[e]);
        }
    })(i)
}

 Teraz klikanie w poszczególne DIVy będzie logowało w konsoli odpowiedni string z tablicy cars.