Kurs JavaScript i TypeScript · Moduł 2: Funkcje, tablice i obiekty
Closures i zakres leksykalny
W tej lekcji7
W Parku Jurajskim każdy wybieg ma ogrodzenie, a każde laboratorium drzwi z kodem dostępu. Podobnie działa zakres (scope) w JavaScript: to niewidzialne ogrodzenie, które decyduje, które zmienne są dostępne w danym miejscu kodu. Jest też problem: jak zbudować licznik obserwacji, którego żaden inny system nie wyzeruje przez pomyłkę? Zmienna globalna jest dostępna dla wszystkich. Rozwiązaniem są closures (domknięcia), czyli dinozaur, który pamięta kod PIN do swojego wybiegu, nawet gdy strażnik dawno odszedł.
Zakres leksykalny (Lexical Scope)
JavaScript używa zakresu leksykalnego: dostęp do zmiennych zależy od miejsca, w którym kod został napisany, a nie od tego, gdzie został wywołany. Wyobraź sobie mapę parku: każdy wybieg (funkcja) widzi główną drogę (zakres globalny) i własne terytorium, ale nie widzi wnętrza innych wybiegów.
1// Zakres globalny - główna droga Parku Jurajskiego
2const parkName = "Jurassic Park";
3
4function enclosureA() {
5 // Zakres funkcji enclosureA - Wybieg A
6 const dinosaurA = "Tyrannosaurus Rex";
7
8 console.log(parkName); // "Jurassic Park" - widzi zakres globalny
9 console.log(dinosaurA); // "Tyrannosaurus Rex" - widzi swój zakres
10
11 function innerFence() {
12 // Zakres zagnieżdżony - wewnętrzne ogrodzenie
13 const babyDino = "Baby Rex";
14
15 console.log(parkName); // "Jurassic Park" - widzi zakres globalny
16 console.log(dinosaurA); // "Tyrannosaurus Rex" - widzi zakres rodzica
17 console.log(babyDino); // "Baby Rex" - widzi swój zakres
18 }
19
20 innerFence();
21 // console.log(babyDino); // BŁĄD! Nie widzi wnętrza innerFence
22}
23
24function enclosureB() {
25 // Zakres funkcji enclosureB - Wybieg B
26 const dinosaurB = "Triceratops";
27
28 console.log(parkName); // "Jurassic Park" - widzi zakres globalny
29 // console.log(dinosaurA); // BŁĄD! Nie widzi wnętrza enclosureA
30}Zasada jest prosta: funkcja wewnętrzna widzi zmienne funkcji zewnętrznej, ale nie odwrotnie. Dinozaur w wewnętrznym wybiegu widzi przez ogrodzenie na zewnątrz, a strażnik z zewnątrz nie zajrzy przez podwójne ściany do środka. Dlatego innerFence odczytuje dinosaurA bez przekazywania argumentu, a enclosureB nie ma do niej dostępu.
Czym jest Closure (Domknięcie)?
Closure to funkcja, która "pamięta" zmienne z zakresu, w którym powstała, nawet gdy ten zakres zakończył działanie. Zobacz to na trackerze obserwacji:
1function createDinoTracker(species) {
2 // Zmienna "species" jest w zakresie createDinoTracker
3 let sightings = 0;
4
5 // Ta funkcja wewnętrzna tworzy CLOSURE
6 // - "pamięta" species i sightings, nawet po zakończeniu createDinoTracker
7 return function reportSighting(location) {
8 sightings++;
9 console.log(species + " widziany w: " + location + " (obserwacja #" + sightings + ")");
10 };
11}
12
13// createDinoTracker zakończyło działanie, ale zwrócona funkcja PAMIĘTA zmienne
14const trackRex = createDinoTracker("T-Rex");
15const trackRaptor = createDinoTracker("Velociraptor");
16
17trackRex("Sektor A"); // "T-Rex widziany w: Sektor A (obserwacja #1)"
18trackRex("Sektor B"); // "T-Rex widziany w: Sektor B (obserwacja #2)"
19trackRaptor("Laboratorium"); // "Velociraptor widziany w: Laboratorium (obserwacja #1)"
20
21// Każde closure ma WŁASNĄ kopię zmiennych!
22// trackRex ma swoje sightings = 2, trackRaptor ma swoje sightings = 1Każde wywołanie createDinoTracker tworzy nowy zakres, więc liczniki obu trackerów są niezależne. Z zewnątrz nikt nie zmieni sightings, bo zmienna nie jest widoczna poza funkcją. Mechanizm przebiega w pięciu krokach:
- Definiujesz funkcję zewnętrzną z lokalnymi zmiennymi.
- W środku definiujesz funkcję wewnętrzną, która z nich korzysta.
- Funkcja zewnętrzna zwraca funkcję wewnętrzną przez
return. - Wywołujesz funkcję zewnętrzną i zapisujesz wynik do zmiennej.
- Wywołujesz zwróconą funkcję, a ona wciąż ma dostęp do zmiennych zewnętrznych.
Praktyczne zastosowania closures
Prywatność danych (enkapsulacja)
Closures pozwalają tworzyć prywatne zmienne, do których nie dostanie się nikt z zewnątrz. Jak system bezpieczeństwa parku, w którym kody dostępu są ukryte w zamkniętej szafie:
1function createSecuritySystem(enclosureName) {
2 // Te zmienne są PRYWATNE - niedostępne z zewnątrz
3 let accessCode = Math.floor(Math.random() * 9000) + 1000;
4 let isLocked = true;
5 const accessLog = [];
6
7 return {
8 unlock(code) {
9 if (code === accessCode) {
10 isLocked = false;
11 accessLog.push("Odblokowano: " + new Date().toLocaleTimeString());
12 console.log(enclosureName + " - ogrodzenie WYŁĄCZONE");
13 } else {
14 accessLog.push("Nieudana próba: " + new Date().toLocaleTimeString());
15 console.log(enclosureName + " - BŁĘDNY KOD!");
16 }
17 },
18 lock() {
19 isLocked = true;
20 accessLog.push("Zablokowano: " + new Date().toLocaleTimeString());
21 console.log(enclosureName + " - ogrodzenie WŁĄCZONE");
22 },
23 getStatus() {
24 return isLocked ? "ZAMKNIĘTY" : "OTWARTY";
25 },
26 getLog() {
27 return [...accessLog]; // Zwraca kopię, nie oryginał
28 }
29 };
30}
31
32const rexSecurity = createSecuritySystem("Wybieg T-Rex");
33// Nie ma sposobu, by bezpośrednio odczytać accessCode!
34// rexSecurity.accessCode → undefined (prywatna zmienna)Zwrócony obiekt ma tylko cztery metody. accessCode, isLocked i accessLog nie są jego właściwościami, lecz zmiennymi z domknięcia, więc zmienić je mogą wyłącznie te metody. getLog() oddaje kopię tablicy, więc wyczyszczenie wyniku nie ruszy prawdziwego dziennika.
Fabryki funkcji (Function Factories)
Closures umożliwiają tworzenie fabryk, czyli funkcji produkujących inne funkcje z wbudowaną konfiguracją. Jak laboratorium genetyczne, które tworzy dinozaury z określonym DNA:
1function createDinoFilter(property, value) {
2 // Closure "zapamiętuje" property i value
3 return function(dinosaur) {
4 return dinosaur[property] === value;
5 };
6}
7
8const isCarnivore = createDinoFilter("diet", "carnivore");
9const isHerbivore = createDinoFilter("diet", "herbivore");
10const isSectorA = createDinoFilter("sector", "A");
11
12const dinosaurs = [
13 { name: "Rex", diet: "carnivore", sector: "A" },
14 { name: "Tricia", diet: "herbivore", sector: "B" },
15 { name: "Blue", diet: "carnivore", sector: "C" }
16];
17
18console.log(dinosaurs.filter(isCarnivore)); // Rex, Blue
19console.log(dinosaurs.filter(isHerbivore)); // Tricia
20console.log(dinosaurs.filter(isSectorA)); // RexTrzy filtry powstały z jednej fabryki, a każdy pamięta własną parę property i value. Najmniejsza fabryka mieści się w trzech liniach:
1function createMultiplier(factor) {
2 return n => n * factor;
3}
4
5const double = createMultiplier(2);
6const triple = createMultiplier(3);
7
8console.log(double(5)); // 10
9console.log(triple(4)); // 12Funkcja strzałkowa zapamiętuje factor, więc double i triple mają ten sam kod, ale różną konfigurację.
IIFE - Immediately Invoked Function Expressions
IIFE (natychmiast wywoływane wyrażenie funkcyjne) to funkcja definiowana i od razu wykonywana. To jednorazowy protokół bezpieczeństwa: uruchamia się, robi swoje i znika, nie zostawiając śladów w zakresie globalnym.
1// Składnia IIFE
2(function() {
3 const secretData = "Lokalizacja ukrytego laboratorium";
4 console.log("IIFE wykonane! Dane: " + secretData);
5})();
6// secretData nie istnieje w zakresie globalnym!
7
8// IIFE ze strzałką
9(() => {
10 const emergencyProtocol = "Ewakuacja sektora 7";
11 console.log("Protokół awaryjny aktywowany: " + emergencyProtocol);
12})();
13
14// IIFE z parametrami
15const dinoReport = (function(parkData) {
16 const processed = parkData.map(d => d.toUpperCase());
17 return processed.join(", ");
18})(["rex", "raptor", "triceratops"]);
19
20console.log(dinoReport); // "REX, RAPTOR, TRICERATOPS"Budowa jest zawsze ta sama: nawias otwierający, function(), ciało { /* kod */ }, nawias zamykający i (), które wywołuje funkcję. Nawias wokół funkcji zamienia ją w wyrażenie, bez niego silnik zgłosi błąd składni. Gdy IIFE zwróci obiekt z metodami, otrzymasz moduł z prywatnym stanem:
1const visitorCounter = (function() {
2 let count = 0; // prywatny stan modułu
3
4 return {
5 increment() { return ++count; },
6 getValue() { return count; }
7 };
8})();
9
10visitorCounter.increment(); // 1
11visitorCounter.increment(); // 2
12console.log(visitorCounter.getValue()); // 2IIFE były bardzo popularne przed ES6, gdy var nie miał zakresu blokowego. Dziś nadal izolują kod, ale w nowym projekcie polecam moduły ES z import i export, a IIFE warto rozpoznawać w starszym kodzie.
Hoisting - var vs let/const
Hoisting (wynoszenie) to mechanizm, w którym deklaracje zmiennych i funkcji są "przenoszone" na początek swojego zakresu. Każde słowo kluczowe zachowuje się jednak inaczej:
1// var - hoisting z wartością undefined
2console.log(dinoVar); // undefined (nie błąd!)
3var dinoVar = "Stegosaurus";
4console.log(dinoVar); // "Stegosaurus"
5
6// let/const - hoisting ale z "Temporal Dead Zone" (TDZ)
7// console.log(dinoLet); // ReferenceError! Zmienna w TDZ
8let dinoLet = "Ankylosaurus";
9console.log(dinoLet); // "Ankylosaurus"
10
11// const - tak samo jak let, plus wymaga natychmiastowej inicjalizacji
12// console.log(dinoConst); // ReferenceError! TDZ
13const dinoConst = "Brachiosaurus";Zmienna z let istnieje od początku bloku, ale do linii deklaracji leży w strefie zakazu wstępu, więc odczyt kończy się ReferenceError, a nie wartością undefined czy null.
var vs let w pętlach - klasyczna pułapka
To jeden z najsłynniejszych problemów JavaScript, gdzie closures i var dają nieoczekiwane wyniki:
1// Problem z var - wszystkie closures dzielą tę samą zmienną
2for (var i = 0; i < 3; i++) {
3 setTimeout(function() {
4 console.log("var: Dinozaur #" + i); // Zawsze "Dinozaur #3"!
5 }, 100);
6}
7
8// Rozwiązanie z let - każda iteracja ma własną kopię
9for (let j = 0; j < 3; j++) {
10 setTimeout(function() {
11 console.log("let: Dinozaur #" + j); // 0, 1, 2 - poprawnie!
12 }, 100);
13}Dlaczego var nie działa? Ma zakres funkcyjny, więc trzy closures odwołują się do jednej zmiennej i, która po pętli ma wartość 3. let ma zakres blokowy i każda iteracja tworzy nową zmienną j. Zanim pojawiło się let, ratowano się IIFE, które zamraża bieżącą wartość:
1// Starsze rozwiązanie: IIFE przechwytuje bieżącą wartość
2for (var k = 0; k < 3; k++) {
3 (function(captured) {
4 setTimeout(function() {
5 console.log("IIFE: Dinozaur #" + captured); // 0, 1, 2
6 }, 100);
7 })(k);
8}Działa, ale let robi to samo bez dodatkowej funkcji, więc to jego wybieraj.
Podsumowanie
| Koncept | Analogia w Parku Jurajskim |
|---|---|
| Zakres globalny | Główna droga parku - widoczna ze wszystkich wybiegów |
| Zakres funkcji | Wybieg dinozaura - widzi na zewnątrz, ale nikt nie widzi do środka |
| Closure | Dinozaur pamięta układ parku nawet po przebudowie ogrodzeń |
| Prywatne zmienne | Kod PIN do ogrodzenia - ukryty w zamkniętym systemie |
| IIFE | Jednorazowy protokół bezpieczeństwa - wykonaj i zapomnij |
| Hoisting var | Dinozaur, który pojawia się na liście przed wykluciem z jaja |
| TDZ (let/const) | Strefa zakazu wstępu do momentu oficjalnego otwarcia wybiegu |
Closures wrócą w module o asynchroniczności, bo każdy callback przekazany do setTimeout czy obietnicy korzysta z domknięcia.
Praktyka
W piaskownicy czekają zakres leksykalny, tracker obserwacji i system bezpieczeństwa z prywatnym dziennikiem. Wywołaj trackery kilka razy i sprawdź, że każdy liczy osobno.
Kod do tej lekcji: index.js
1// Closures i zakres leksykalny - Park Jurajski
2console.log("=== CLOSURES W PARKU JURAJSKIM ===\n");
3
4// ================================================
5// 1. ZAKRES LEKSYKALNY - ogrodzenia parku
6// ================================================
7console.log("--- 1. Zakres leksykalny ---");
8
9const parkName = "Jurassic Park"; // Zakres globalny
10
11function enclosureA() {
12 const dinosaurA = "Tyrannosaurus Rex"; // Zakres enclosureA
13
14 function innerFence() {
15 const babyDino = "Baby Rex"; // Zakres innerFence
16 // Widzi WSZYSTKO: swoje zmienne + rodzica + globalne
17 console.log("innerFence widzi: " + parkName + ", " + dinosaurA + ", " + babyDino);
18 }
19
20 innerFence();
21 // console.log(babyDino); // BLAD! Nie widzi wnetrza innerFence
22 console.log("enclosureA widzi: " + parkName + ", " + dinosaurA);
23}
24
25enclosureA();
26
27// ================================================
28// 2. CLOSURE - dinozaur pamieta swoje otoczenie
29// ================================================
30console.log("\n--- 2. Closure - tracker dinozaurow ---");
31
32function createDinoTracker(species) {
33 let sightings = 0; // Ta zmienna zyje w closure!
34
35 return function reportSighting(location) {
36 sightings++; // Closure pamięta i modyfikuje sightings
37 console.log(species + " widziany w: " + location + " (obserwacja #" + sightings + ")");
38 };
39}
40
41const trackRex = createDinoTracker("T-Rex");
42const trackRaptor = createDinoTracker("Velociraptor");
43
44trackRex("Sektor A");
45trackRex("Sektor B");
46trackRaptor("Laboratorium");
47trackRex("Sektor C");
48// Kazdy tracker ma WLASNA kopie zmiennej sightings!
49
50// ================================================
51// 3. PRYWATNOSC DANYCH - system bezpieczenstwa
52// ================================================
53console.log("\n--- 3. Prywatne zmienne (enkapsulacja) ---");
54
55function createSecuritySystem(enclosureName) {
56 let isLocked = true; // PRYWATNA zmienna
57 const log = [];
58
59 return {
60 unlock() {
61 isLocked = false;
62 log.push("Odblokowano o " + new Date().toLocaleTimeString());
63 console.log(enclosureName + " - ogrodzenie WYLACZONE");
64 },
65 lock() {
66 isLocked = true;
67 log.push("Zablokowano o " + new Date().toLocaleTimeString());
68 console.log(enclosureName + " - ogrodzenie WLACZONE");
69 },
70 getStatus() {
71 return enclosureName + ": " + (isLocked ? "ZAMKNIETY" : "OTWARTY");
72 },
73 getLog() { return [...log]; }
74 };
75}
76
77const rexSecurity = createSecuritySystem("Wybieg T-Rex");
78console.log(rexSecurity.getStatus());
79rexSecurity.unlock();
80console.log(rexSecurity.getStatus());
81rexSecurity.lock();
82console.log("Log:", rexSecurity.getLog());
83// rexSecurity.isLocked -> undefined (prywatna!)
84
85// ================================================
86// 4. FABRYKA FUNKCJI
87// ================================================
88console.log("\n--- 4. Fabryka funkcji ---");
89
90function createDinoFilter(property, value) {
91 return function(dinosaur) {
92 return dinosaur[property] === value;
93 };
94}
95
96const dinosaurs = [
97 { name: "Rex", diet: "carnivore", sector: "A" },
98 { name: "Tricia", diet: "herbivore", sector: "B" },
99 { name: "Blue", diet: "carnivore", sector: "C" },
100 { name: "Steggy", diet: "herbivore", sector: "A" }
101];
102
103const isCarnivore = createDinoFilter("diet", "carnivore");
104const isSectorA = createDinoFilter("sector", "A");
105
106console.log("Miesozercy:", dinosaurs.filter(isCarnivore).map(d => d.name));
107console.log("Sektor A:", dinosaurs.filter(isSectorA).map(d => d.name));
108
109// ================================================
110// 5. IIFE - jednorazowy protokol
111// ================================================
112console.log("\n--- 5. IIFE ---");
113
114const emergencyReport = (function(dinos) {
115 const carnivores = dinos.filter(d => d.diet === "carnivore");
116 const herbivores = dinos.filter(d => d.diet === "herbivore");
117 return "Raport: " + carnivores.length + " miesozercow, " + herbivores.length + " roslinozercy";
118})(dinosaurs);
119
120console.log(emergencyReport);
121
122// ================================================
123// 6. HOISTING - var vs let
124// ================================================
125console.log("\n--- 6. Hoisting ---");
126
127// var - hoisting z undefined
128console.log("var przed deklaracja:", typeof dinoVar); // undefined
129var dinoVar = "Stegosaurus";
130console.log("var po deklaracji:", dinoVar);
131
132// let - Temporal Dead Zone
133// console.log(dinoLet); // ReferenceError!
134let dinoLet = "Ankylosaurus";
135console.log("let po deklaracji:", dinoLet);
136
137// var vs let w petli
138console.log("\nPetla z let:");
139for (let j = 0; j < 3; j++) {
140 setTimeout(() => console.log(" let: Dinozaur #" + j), 10);
141}Zapamiętaj: closure to dinozaur, który pamięta swój wybieg, czyli funkcja zabierająca ze sobą zmienne z miejsca, w którym powstała.
Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Co zobaczymy w konsoli po wykonaniu poniższego kodu? function outer() { const x = 10; function inner() { console.log(x); } inner(); } outer();
2. Czym jest closure (domknięcie) w JavaScript?
3. Co się stanie, gdy spróbujemy użyć zmiennej zadeklarowanej przez let PRZED jej deklaracją? console.log(dinoName); let dinoName = "Rex";
Zadania praktyczne w grze
- Edytor kodu
Napisz funkcję createDinoTracker(species), która trzyma w domknięciu prywatny licznik sightings i tablicę history i zwraca obiekt z metodami: reportSighting(location) (zwiększa licznik i dopisuje lokalizację do historii), getSightings() (zwraca liczbę obserwacji) i getHistory() (zwraca kopię historii). Napisz też createEnclosure(name, maxCapacity) z prywatną tablicą residents i metodami: addDinosaur(dinoName) (dodaje dinozaura i zwraca true, a gdy brak miejsca, zwraca false), removeDinosaur(dinoName), getPopulation() (zwraca kopię tablicy) i getStatus() (zwraca tekst 'name: X/maxCapacity', np. 'Paddock T-Rex: 1/2').
- Układanie w pionie
Uporządkuj kroki powstawania i działania closure w JavaScript:
- Klikanie w kolejności
Zbuduj poprawne IIFE (Immediately Invoked Function Expression) klikając elementy w odpowiedniej kolejności:
- Edytor kodu
Napisz fabrykę createFilter(property, value), która zwraca funkcję sprawdzającą, czy obiekt[property] === value, oraz fabrykę createWeightConverter(multiplier), która zwraca funkcję mnożącą wagę w kilogramach przez multiplier. Następnie za pomocą IIFE utwórz licznik dinoCounter z prywatną zmienną count i metodami increment(), decrement() i getValue().