Kurs JavaScript i TypeScript · Moduł 2: Funkcje, tablice i obiekty

Hoisting w JavaScript

17 min czytania
W tej lekcji8

Witaj z powrotem w Parku Jurajskim! W lekcji o zasięgu zmiennych sprawdzaliśmy, gdzie żyje zmienna, czyli jaki ma zasięg. Dzisiaj odpowiemy na pytanie równie ważne: od którego momentu zmienna albo funkcja w ogóle istnieje. Mechanizm, który o tym decyduje, nazywa się hoistingiem, a jego znajomość chroni przed najbardziej podstępną kategorią błędów w systemach parku - takich, które niczego nie wysadzają w powietrze, tylko po cichu podają złą wartość. Zmienna, która istnieje, ale nic nie zawiera, bywa groźniejsza niż zmienna, której nie ma wcale.

Czym jest hoisting?

Hoisting (po polsku "wynoszenie") to zachowanie JavaScriptu polegające na przeniesieniu deklaracji zmiennych i funkcji na górę ich zasięgu - funkcyjnego albo globalnego - jeszcze w fazie kompilacji, zanim wykona się pierwsza linia kodu. Silnik najpierw przegląda cały kod i spisuje, jakie nazwy w nim występują, a dopiero potem zaczyna go wykonywać. Dzięki temu do części zmiennych i funkcji możemy odwołać się wcześniej, niż zostały zapisane w pliku.

To trochę jak spis inwentarza sporządzany, zanim dinozaury trafią do zagród: kompilator JavaScriptu najpierw notuje, "czego się spodziewać", a dopiero potem wykonuje konkretne operacje, linia po linii. Na górę wędrują wyłącznie deklaracje - nigdy wartości, które im przypisujemy.

Każda zmienna przechodzi ten sam cykl życia. Silnik najpierw parsuje kod, potem tworzy kontekst wykonania i rezerwuje w nim pamięć na zadeklarowane nazwy (to właśnie moment hoistingu), a dopiero wtedy wykonuje kod: zmienna dostaje wartość i jest używana. Gdy do wartości nie prowadzi już żadna referencja, sprzątaniem zajmuje się garbage collector. W fazie oznaczania (mark) zaznacza wszystko, do czego program nadal może dotrzeć, a resztę uznaje za przeznaczoną do usunięcia, po czym w fazie sprzątania (sweep) zwalnia jej pamięć.

Hoisting funkcji

Deklaracje funkcji wynoszone są w całości: razem z nazwą podróżuje też ciało funkcji. Możemy więc wywołać funkcję, zanim pojawi się w pliku. W zarządzaniu parkiem to bardzo wygodne, bo pozwala trzymać główną logikę na górze, a szczegóły techniczne spychać na dół.

1// Możemy wywołać funkcję przed jej zdefiniowaniem
2checkSecurity("Zagroda T-Rex");  // "Sprawdzam bezpieczeństwo: Zagroda T-Rex"
3
4// Deklaracja funkcji jest hoistowana
5function checkSecurity(location) {
6  console.log(`Sprawdzam bezpieczeństwo: ${location}`);
7  return true;
8}

Działa to dlatego, że w fazie kompilacji JavaScript wynosi na górę zasięgu całą deklarację - nazwę wraz z ciałem. Zanim wykona się pierwsza instrukcja, checkSecurity jest już kompletną, gotową do wywołania funkcją, a nie pustym miejscem czekającym na uzupełnienie. To jedyny przypadek w całym języku, w którym na górę zasięgu trafia razem z nazwą także wartość, bo funkcja też jest wartością. Wszystkie pozostałe konstrukcje, które za chwilę zobaczymy, wynoszą samą nazwę i nic poza nią.

Uwaga na wyrażenia funkcyjne!

W całości wynoszone są tylko deklaracje funkcji. Wyrażenia funkcyjne, czyli funkcje przypisane do zmiennej, podlegają regułom hoistingu zmiennych: sama zmienna trafia na górę, ale przypisana do niej funkcja już nie.

1// To NIE zadziała prawidłowo
2// feedDinosaur("Rex");  // TypeError: feedDinosaur is not a function
3
4// Hoistowana jest tylko deklaracja zmiennej, nie przypisanie funkcji
5var feedDinosaur = function(dinoName) {
6  console.log(`Karmienie dinozaura: ${dinoName}`);
7};
8
9// Od tej linii możemy już wywołać funkcję
10feedDinosaur("Rex");  // "Karmienie dinozaura: Rex"

Przeczytaj uważnie komunikat błędu, bo to cenna wskazówka. Nie dostajemy ReferenceError, tylko TypeError: feedDinosaur is not a function - nazwa feedDinosaur już istnieje, po prostu trzyma w tym momencie undefined, a undefined nie da się wywołać jak funkcji. Ta drobna różnica potrafi skrócić debugowanie z godziny do minuty: ReferenceError kieruje nas do literówki albo zapomnianego importu, a TypeError do kolejności linii w pliku. Deklaracja funkcji jest dostępna w całym swoim zasięgu, wyrażenie funkcyjne dopiero od linii z przypisaniem.

Funkcje strzałkowe działają tak samo

Funkcje strzałkowe również są wyrażeniami, więc ich ciało nigdy nie jest wynoszone. Ponieważ przypisujemy je zwykle do const albo let, błąd przy zbyt wczesnym wywołaniu okazuje się jeszcze bardziej stanowczy niż przy var.

1// checkFence();  // ReferenceError: Cannot access checkFence before initialization
2
3const checkFence = () => "Ogrodzenie OK";
4
5console.log(checkFence());  // "Ogrodzenie OK"

Różnica jest istotna: przy var dostaliśmy TypeError, tutaj ReferenceError. Oba komunikaty znaczą to samo - "za wcześnie" - ale wersja z const zatrzymuje program dokładnie w miejscu pomyłki, zamiast puścić dalej wartość undefined, która wybuchnie kilkadziesiąt linii później, w zupełnie niewinnym fragmencie kodu. Stąd praktyczna reguła: przed linią definicji można używać wyłącznie funkcji zadeklarowanych przez function nazwa() {}. Wszystko, co przypisujemy do zmiennej - klasyczne wyrażenie funkcyjne czy strzałka - działa dopiero poniżej.

Hoisting zmiennych

Przy zmiennych hoisting zachowuje się różnie, zależnie od słowa kluczowego (var, let, const). Stąd bierze się większość niespodzianek, więc rozłóżmy oba przypadki na czynniki pierwsze.

Hoisting zmiennych deklarowanych przez var

Zmienne deklarowane przez var są wynoszone i od razu inicjalizowane wartością undefined, więc odczyt przed deklaracją jest w pełni legalny:

1console.log(dinosaurCount);  // undefined (nie błąd!)
2var dinosaurCount = 15;
3console.log(dinosaurCount);  // 15

Nic się nie zepsuło - i na tym właśnie polega problem. Program leci dalej z wartością, której nikt nie zaplanował, a undefined w liczniku dinozaurów zamienia się w NaN w każdym obliczeniu, do którego trafi. Raport pokaże NaN zamiast liczby zwierząt w zagrodzie, w której siedzą trzy, i nikt nie dostanie żadnego ostrzeżenia. Łatwiej to zrozumieć, gdy wyobrazimy sobie, że przed uruchomieniem JavaScript przepisuje powyższy kod do takiej postaci:

1var dinosaurCount;           // Hoisting - deklaracja wędruje na górę
2console.log(dinosaurCount);  // undefined
3dinosaurCount = 15;          // Przypisanie zostaje tam, gdzie je napisaliśmy
4console.log(dinosaurCount);  // 15

Deklaracja wskoczyła na górę, przypisanie zostało dokładnie tam, gdzie je zapisaliśmy. Cała reguła hoistingu mieści się w jednym zdaniu: deklaracja wędruje w górę, wartość nie rusza się z miejsca. Odcinek między tymi dwoma punktami to okno, w którym zmienna jest osiągalna, ale pusta. Czujnik parku raportujący undefined wygląda w logach niepokojąco podobnie do czujnika, który nie raportuje niczego - a to dwie zupełnie różne sytuacje, jedna oznacza spokój, druga zerwane ogrodzenie.

Hoisting zmiennych deklarowanych przez let i const

Zmienne deklarowane przez let i const (wprowadzone w ES6) także są hoistowane, ale nie dostają wartości undefined. Zamiast tego trafiają do temporal dead zone, czyli martwej strefy czasowej - okresu od rozpoczęcia wykonywania bloku aż do linii, w której zmienna faktycznie zostaje zadeklarowana. Sięgnięcie po nią w tym czasie kończy się błędem.

1// To spowoduje błąd:
2// console.log(securityLevel);  // ReferenceError: Cannot access securityLevel before initialization
3let securityLevel = "Wysoki";
4console.log(securityLevel);  // "Wysoki"
5
6// To samo dotyczy const:
7// console.log(parkName);  // ReferenceError: Cannot access parkName before initialization
8const parkName = "Jurassic Park";
9console.log(parkName);  // "Jurassic Park"

Dwa komunikaty błędów trzeba umieć od siebie odróżnić:

  • zmienna, która nigdy nie została zadeklarowana: ReferenceError: xyz is not defined
  • zmienna let lub const w temporal dead zone: ReferenceError: Cannot access 'xyz' before initialization

To rozróżnienie jest prezentem dla debugującego. Pierwszy przypadek oznacza, że zmiennej nie ma nigdzie - najpewniej literówka albo zapomniany import. Drugi mówi, że zmienna istnieje, tylko sięgnęliśmy po nią za wcześnie, więc wystarczy przenieść odczyt poniżej deklaracji. Wyobraź sobie funkcję na dwieście linii, w której wartość jest czytana na górze, a przypisywana na dole: z var dostajemy ciche undefined i godzinę szukania po omacku, z let program zatrzymuje się natychmiast i wskazuje palcem konkretną linię. Temporal dead zone to nie ograniczenie, tylko system wczesnego ostrzegania.

Klasy też siedzą w temporal dead zone

Klasy zachowują się jak let i const, a nie jak deklaracje funkcji. Ich nazwa jest wynoszona, ale dopóki silnik nie wykona ciała klasy, każda próba użycia kończy się błędem.

1// const dino = new Dinosaur("Rex");  // ReferenceError: Cannot access Dinosaur before initialization
2
3class Dinosaur {
4  constructor(name) {
5    this.name = name;
6  }
7}
8
9const dino = new Dinosaur("Rex");  // OK
10console.log(dino.name);  // "Rex"

Reguła "funkcje można definiować poniżej miejsca użycia" nie rozciąga się więc na klasy - klasa musi powstać przed pierwszym new, które tworzy jej instancję. Jeśli z przyzwyczajenia zepchniesz definicję klasy na koniec pliku, to właśnie ten komunikat zobaczysz w konsoli, a lekarstwo jest banalne: przenieś klasę nad kod, który ją instancjonuje. W plikach parku daje to prostą kolejność - najpierw modele zwierząt i urządzeń, potem logika, która z nich korzysta, i nigdy odwrotnie.

Hoisting a zasięg zmiennych

Hoisting jest ściśle spleciony z zasięgiem zmiennych, nazywanym też ich zakresem. Deklaracje nie wędrują na górę pliku, tylko na górę swojego zasięgu, a to, który zasięg jest "swój", zależy od słowa kluczowego: var trzyma się zasięgu funkcyjnego, let i const - blokowego. W zasięgu globalnym reguła pozostaje ta sama, tyle że górą jest po prostu początek programu.

Przykład dla var (zasięg funkcyjny)

var całkowicie ignoruje klamry if-a czy pętli: deklaracja ląduje na górze funkcji, która ją zawiera, niezależnie od tego, w którym miejscu ją zapisaliśmy.

1function monitorSystem() {
2  console.log(systemStatus);  // undefined (hoisting w obrębie funkcji)
3
4  if (true) {
5    var systemStatus = "Online";
6  }
7
8  console.log(systemStatus);  // "Online"
9}
10
11monitorSystem();
12// console.log(systemStatus);  // ReferenceError: systemStatus is not defined (poza zasięgiem funkcji)

Blok if w ogóle nie utworzył tutaj nowego zasięgu dla systemStatus. Deklaracja wskoczyła na górę monitorSystem, więc zmienną da się odczytać jeszcze przed blokiem (jest wtedy pusta), a po bloku trzyma już przypisaną wartość. Poza funkcją znika kompletnie, bo jedyną granicą, którą var respektuje, jest funkcja. Dwa console.log z tą samą nazwą dają zatem dwa różne wyniki - nie dlatego, że zmienne są dwie, tylko dlatego, że patrzymy na jedną zmienną raz przed przypisaniem, a raz po nim.

Pętle pokazują tę różnicę najwyraźniej

Najlepszym poligonem do porównania obu zasięgów są pętle, bo licznik jest w nich deklarowany i używany w jednym miejscu. Sprawdź, co zostaje po pętli, w której licznik powstaje przez var, a co po pętli z let.

1for (var index = 0; index < 3; index++) {
2  // obchód zagród
3}
4console.log(index);  // 3 - var przeżywa pętlę (zasięg funkcyjny)
5
6for (let step = 0; step < 3; step++) {
7  // obchód zagród
8}
9// console.log(step);  // ReferenceError - let zostaje w zasięgu blokowym

Po zakończeniu pierwszej pętli index nadal ma wartość 3, bo należy do zasięgu funkcyjnego i spokojnie przeżywa zamykającą klamrę. Zmienna step z drugiej pętli ma zasięg blokowy i znika razem z pętlą, więc odczyt poniżej kończy się błędem ReferenceError. Ta różnica stoi za słynnymi kłopotami z licznikiem w pętlach z opóźnieniem: przy var wszystkie wywołania widzą tę samą, końcową wartość, przy let każda iteracja dostaje własną kopię licznika i zachowuje swoją.

Przykład dla let i const (zasięg blokowy)

Przy let i const każda para klamer tworzy nowy zasięg, a temporal dead zone dotyczy właśnie tego bloku, a nie całej funkcji.

1function checkEnclosures() {
2  // console.log(enclosureStatus);  // ReferenceError (temporal dead zone)
3
4  if (true) {
5    let enclosureStatus = "Secure";
6    console.log(enclosureStatus);  // "Secure"
7  }
8
9  // console.log(enclosureStatus);  // ReferenceError (poza zasięgiem bloku)
10}
11
12checkEnclosures();

Obie zakomentowane linie skończyłyby się błędem, ale z zupełnie różnych powodów: pierwsza dlatego, że zmienna siedzi jeszcze w temporal dead zone, druga dlatego, że blok if się skończył i zmienna przestała istnieć. Klamry są tu prawdziwym murem i dokładnie o to nam chodzi - zmienna blokowa zostaje w swojej zagrodzie. W ćwiczeniu, które czeka Cię zaraz po tej lekcji, poćwiczysz zasięg zmiennych w różnych kontekstach: stworzysz zmienne z różnym zasięgiem - globalnym, funkcyjnym i blokowym - i będziesz obserwować ich zachowanie.

Praktyczne przykłady hoistingu w Parku Jurajskim

Przykład 1: System monitorowania dinozaurów

Hoisting funkcji pozwala uporządkować plik według ważności, a nie według kolejności wykonania. Główna procedura idzie na górę, funkcje pomocnicze, z których korzysta, na sam dół - i wszystko nadal działa.

1// System monitorowania dinozaurów
2
3// Funkcje główne są zdefiniowane na górze dla przejrzystości
4function initMonitoringSystem() {
5  console.log("Inicjalizacja systemu monitorowania dinozaurów...");
6
7  // Możemy wywołać te funkcje, choć są zdefiniowane później
8  const healthStatus = checkDinosaurHealth("Rex");
9  const locationData = trackDinosaur("Blue");
10
11  return {
12    health: healthStatus,
13    location: locationData,
14    systemStatus: "Online"
15  };
16}
17
18// Wywołanie głównej funkcji
19const systemStatus = initMonitoringSystem();
20console.log(systemStatus);
21
22// Funkcje pomocnicze zdefiniowane na dole pliku
23function checkDinosaurHealth(dinoName) {
24  console.log(`Sprawdzanie stanu zdrowia dinozaura: ${dinoName}`);
25  return "Zdrowy";
26}
27
28function trackDinosaur(dinoName) {
29  console.log(`Śledzenie dinozaura: ${dinoName}`);
30  return { x: 135, y: 270, area: "Sektor B" };
31}

Wywołanie initMonitoringSystem wypada w linii, do której żadna z funkcji pomocniczych jeszcze "nie doszła", a mimo to kod wykonuje się bez zgrzytu. To czysty hoisting deklaracji funkcji: obie były kompletne, zanim ruszyła pierwsza instrukcja. W czasie działania programu nic się już magicznie nie przesuwa, a zysk jest czysto ludzki - kto otworzy ten plik, najpierw przeczyta, co system robi, a dopiero potem, w jaki sposób.

Przykład 2: Pułapka hoistingu

Ten sam mechanizm potrafi zadziałać przeciwko nam. Hoisting staje się zdradliwy, gdy funkcja wewnętrzna deklaruje zmienną o nazwie, która istnieje już w zasięgu zewnętrznym. Przeczytaj kod uważnie i spróbuj przewidzieć wynik, zanim zajrzysz do wyjaśnienia.

1// Pułapka związana z hoistingiem
2
3// Funkcja sprawdzająca zabezpieczenia
4function checkSecurity() {
5  var fenceStatus = "Offline"; // Hoistowana na górę funkcji
6
7  function enableFences() {
8    // Lokalna zmienna jest hoistowana na górę enableFences,
9    // ale aż do linii inicjalizacji nie ma żadnej wartości
10    console.log("Aktualny status płotu przed zmianą:", fenceStatus); // undefined!
11
12    // Lokalna zmienna fenceStatus przesłania tę z wyższego zakresu
13    var fenceStatus = "Online";
14    console.log("Nowy status płotu:", fenceStatus); // "Online"
15  }
16
17  enableFences();
18  console.log("Status płotu w głównej funkcji:", fenceStatus); // "Offline" (nie zmieniono)
19}
20
21checkSecurity();

Kryje się tu subtelna pułapka: console.log wewnątrz enableFences wypisuje undefined, ponieważ lokalna zmienna fenceStatus została wyniesiona na górę funkcji wewnętrznej i od pierwszej linii przesłania tę z zasięgu zewnętrznego, choć wartość dostaje dopiero niżej. Zewnętrzna zmienna również nigdy się nie zmienia - do końca ma wartość "Offline". W prawdziwym parku wygląda to tak, że panel operatora ogłasza włączone ogrodzenia, a główna funkcja bezpieczeństwa nadal uważa je za wyłączone.

Przykład 3: Lepsze podejście z let i const

Zamiana var na let i const likwiduje całą tę klasę problemów, bo zbyt wczesny odczyt zatrzymuje program, zamiast po cichu produkować undefined.

1// Lepsze podejście z użyciem let i const
2
3function parkOperations() {
4  // Używamy const dla wartości, które nie powinny się zmieniać
5  const maxVisitors = 2000;
6
7  // Używamy let dla zmiennych, które będą się zmieniać
8  let currentVisitors = 0;
9
10  function admitVisitors(count) {
11    // Przy let i const odczyt zmiennej przed deklaracją kończy się błędem,
12    // co znacznie ułatwia wychwycenie problemu
13
14    // To by spowodowało błąd:
15    // console.log(availableSpace);  // ReferenceError: Cannot access before initialization
16
17    // Najpierw deklarujemy, potem używamy
18    const availableSpace = maxVisitors - currentVisitors;
19    if (count <= availableSpace) {
20      currentVisitors += count;
21      console.log(`Przyjęto ${count} odwiedzających. Aktualnie w parku: ${currentVisitors}`);
22      return true;
23    }
24
25    console.log(`Za dużo odwiedzających! Dostępne miejsca: ${availableSpace}`);
26    return false;
27  }
28
29  // Testujemy naszą funkcję
30  admitVisitors(500);  // Przyjęto 500 odwiedzających
31  admitVisitors(1000); // Przyjęto 1000 odwiedzających
32  admitVisitors(700);  // Za dużo odwiedzających! Dostępne miejsca: 500
33
34  return currentVisitors;
35}
36
37console.log(`Łączna liczba odwiedzających: ${parkOperations()}`);

Sprawdźmy, co się tu nie zmieniło: maxVisitors i currentVisitors nadal są hoistowane, bo hoistingowi podlega każda deklaracja. Zmieniła się reakcja na pomyłkę - temporal dead zone przerabia zbyt wczesny odczyt na głośny błąd zamiast na dyskretne undefined. Każda wartość powstaje dokładnie tam, gdzie jest potrzebna, i ani chwili wcześniej, dzięki czemu logikę bramek czyta się z góry na dół, bez trzymania reguł hoistingu w głowie. O to właśnie chodzi w praktyce: nie o wykucie mechanizmu, tylko o pisanie kodu, w którym ten mechanizm nigdy nie zaskakuje.

Praktyczne wskazówki dotyczące hoistingu

  1. Zawsze deklaruj zmienne na górze ich zasięgu - to minimalizuje problemy związane z hoistingiem i sprawia, że kod czyta się dużo łatwiej.

  2. Preferuj let i const zamiast var - błędy z temporal dead zone są nieporównanie łatwiejsze do zdiagnozowania niż tajemnicze wartości undefined, które ujawniają się kilkaset linii dalej.

  3. Deklaruj funkcje przed ich użyciem - hoisting funkcji działa, ale naturalna kolejność czytania i tak pozostaje najczytelniejsza dla osoby, która wróci do tego kodu za pół roku.

  4. Unikaj deklaracji funkcji wewnątrz bloków warunkowych - zachowanie potrafi się różnić między środowiskami, więc bezpieczniej użyć wtedy wyrażenia funkcyjnego przypisanego do zmiennej.

  5. Pamiętaj, że hoisting dotyczy deklaracji, a nie inicjalizacji - przypisanie wartości zachodzi dokładnie tam, gdzie je napisaliśmy, nigdy wcześniej.

Większy przykład: system zarządzania incydentami w parku

Poniżej znajdziesz pełniejszy system zarządzania incydentami w Parku Jurajskim, który zbiera w jednym miejscu wszystko z tej lekcji: hoistowane deklaracje funkcji, zmienną var na poziomie modułu oraz let i const użyte do stanu, który się zmienia, i do stanu, który zmieniać się nie może.

1// System zarządzania incydentami w Parku Jurajskim
2
3// Wykorzystanie hoistingu - deklarujemy zmienne na początku
4var activeIncidents = [];
5let alertLevel = "Normalny";
6const MAX_INCIDENTS = 5;
7
8// Główna funkcja do inicjalizacji systemu
9function initIncidentSystem() {
10  // Ta zmienna jest hoistowana tylko w obrębie tej funkcji
11  var systemStatus = "Inicjalizacja";
12
13  // Wykorzystujemy hoisting funkcji - możemy je wywołać przed definicją
14  logSystemStatus();
15  resetIncidents();
16
17  systemStatus = "Online";
18  logSystemStatus();
19
20  // Funkcja lokalna - widoczna tylko wewnątrz initIncidentSystem
21  function logSystemStatus() {
22    console.log(`Status systemu: ${systemStatus}`);
23  }
24
25  return {
26    reportIncident,      // Referencja do funkcji zdefiniowanej poniżej
27    checkAlertLevel,     // Funkcja zdefiniowana poniżej
28    getActiveIncidents,  // Funkcja zdefiniowana poniżej
29    resetIncidents       // Funkcja zdefiniowana wewnątrz initIncidentSystem
30  };
31
32  // Funkcja lokalna, którą zwracamy jako część API
33  function resetIncidents() {
34    console.log("Resetowanie listy incydentów...");
35    activeIncidents = [];
36    updateAlertLevel();
37  }
38}
39
40// Deklaracje funkcji są hoistowane, więc można je wywołać przed definicją
41// Funkcja do zgłaszania nowego incydentu
42function reportIncident(location, type, severity) {
43  console.log(`Zgłoszenie incydentu: ${type} w lokalizacji ${location} (poziom zagrożenia: ${severity})`);
44
45  const incident = {
46    id: generateIncidentId(),
47    location,
48    type,
49    severity,
50    timestamp: new Date().toISOString(),
51    status: "Aktywny"
52  };
53
54  activeIncidents.push(incident);
55
56  if (activeIncidents.length > MAX_INCIDENTS) {
57    declareEmergency();
58  } else {
59    updateAlertLevel();
60  }
61
62  return incident;
63}
64
65// Generowanie unikalnego ID incydentu
66function generateIncidentId() {
67  return `INC-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
68}
69
70// Aktualizacja poziomu alertu na podstawie aktywnych incydentów
71function updateAlertLevel() {
72  const highSeverityCount = activeIncidents.filter(inc => inc.severity === "Wysoki").length;
73
74  if (highSeverityCount >= 3) {
75    alertLevel = "Krytyczny";
76  } else if (highSeverityCount > 0 || activeIncidents.length >= 3) {
77    alertLevel = "Podwyższony";
78  } else if (activeIncidents.length > 0) {
79    alertLevel = "Ostrożność";
80  } else {
81    alertLevel = "Normalny";
82  }
83
84  console.log(`Poziom alertu zaktualizowany: ${alertLevel}`);
85}
86
87// Funkcja wywoływana w przypadku zbyt wielu incydentów
88function declareEmergency() {
89  alertLevel = "Ewakuacja";
90  console.log("UWAGA! Zbyt wiele aktywnych incydentów. Ogłaszamy ewakuację parku!");
91  // Kod do inicjacji protokołów ewakuacji...
92}
93
94// Funkcja do sprawdzania aktualnego poziomu alertu
95function checkAlertLevel() {
96  return {
97    level: alertLevel,
98    incidentCount: activeIncidents.length,
99    timestamp: new Date().toISOString()
100  };
101}
102
103// Funkcja zwracająca listę aktywnych incydentów
104function getActiveIncidents() {
105  return [...activeIncidents]; // Zwracamy kopię, aby nikt nie zmodyfikował oryginału
106}
107
108// Inicjalizacja systemu
109const incidentSystem = initIncidentSystem();
110
111// Testowanie systemu
112console.log("===== TESTOWANIE SYSTEMU ZARZĄDZANIA INCYDENTAMI =====");
113
114// Sprawdzanie początkowego stanu
115console.log("Początkowy poziom alertu:", incidentSystem.checkAlertLevel().level);
116
117// Zgłaszanie incydentów
118incidentSystem.reportIncident("Zagroda T-Rex", "Uszkodzenie ogrodzenia", "Wysoki");
119incidentSystem.reportIncident("Centrum Gości", "Brak zasilania", "Średni");
120
121// Sprawdzanie stanu po zgłoszeniach
122console.log("Aktywne incydenty:", incidentSystem.getActiveIncidents().length);
123console.log("Aktualny poziom alertu:", incidentSystem.checkAlertLevel().level);
124
125// Dodawanie kolejnych incydentów wysokiego priorytetu
126incidentSystem.reportIncident("Laboratorium", "Wyciek materiału genetycznego", "Wysoki");
127incidentSystem.reportIncident("Zagroda Velociraptorów", "Próba ucieczki", "Wysoki");
128
129// Powinno wywołać stan ewakuacji
130incidentSystem.reportIncident("Sektor B", "Dinozaur poza zagrodą", "Wysoki");
131incidentSystem.reportIncident("Centrum Operacyjne", "Awaria systemów bezpieczeństwa", "Wysoki");
132
133// Reset systemu
134incidentSystem.resetIncidents();
135console.log("Po resecie - poziom alertu:", incidentSystem.checkAlertLevel().level);

Prześledź kolejność zdarzeń w tym pliku. initIncidentSystem wywołuje logSystemStatus i resetIncidents, zanim którakolwiek z nich pojawi się w kodzie, a resetIncidents stoi nawet za instrukcją return - mimo to wszystko działa, bo deklaracje funkcji są kompletne przed wykonaniem pierwszej linii ciała. Ze zmiennymi jest inaczej: var systemStatus można odczytać od samego początku funkcji, ale wartość "Inicjalizacja" dostaje dopiero w swojej linii, a activeIncidents, alertLevel i MAX_INCIDENTS celowo trafiły na samą górę pliku, żeby nikt nie sięgnął po nie za wcześnie.

Podsumowanie

Hoisting to ważny koncept JavaScriptu, który kształtuje sposób wykonywania naszego kodu:

  1. Deklaracje funkcji są hoistowane w całości - można ich używać przed deklaracją.

  2. Zmienne var są hoistowane i inicjalizowane wartością undefined - można się do nich odwołać wcześniej, ale do momentu przypisania nie niosą żadnej użytecznej wartości.

  3. Zmienne let i const (a razem z nimi klasy) są hoistowane, ale zostają w temporal dead zone - próba odczytu przed deklaracją kończy się błędem.

  4. Wyrażenia funkcyjne, w tym funkcje strzałkowe, podlegają regułom zmiennych - zmienna jest hoistowana, przypisana funkcja już nie.

Zrozumienie hoistingu jest kluczowe dla pisania przewidywalnego kodu JavaScript. Działa to dokładnie jak w samym Parku Jurajskim - znajomość zasad i protokołów wystarcza, żeby dzień minął bez niespodzianek i niebezpiecznych sytuacji.

"Hoisting w JavaScripcie przypomina poranne rozruchy parku" - mówi Dr. Rex. "Zanim przyjadą zwiedzający, zanim ruszy kod, część przygotowań dzieje się sama: protokoły są gotowe, procedury na miejscu. Ale prawdziwe dane trzeba dopiero zebrać na bieżąco, a sięganie po nie za wcześnie nie powie ci absolutnie nic!"

W jednej z następnych lekcji weźmiemy na warsztat słowo kluczowe this - kolejny fundamentalny, a przy tym potrafiący nieźle namieszać element JavaScriptu.

Kod do tej lekcji: index.js
1// Hoisting w JavaScript - Park Jurajski
2console.log("Laboratorium Hoisting");
3console.log("Zrozumienie wynoszenia w JavaScript\n");
4
5// ===========================================
6// 1. CZYM JEST HOISTING?
7// ===========================================
8console.log("=== 1. Wprowadzenie do hoisting ===");
9
10// JavaScript "wynosi" deklaracje na górę zasięgu
11// Możemy używać funkcji przed jej deklaracją!
12
13roarDinosaur("T-Rex"); // Działa!
14
15function roarDinosaur(name) {
16  console.log(`${name} says: ROAAARRR!`);
17}
18
19roarDinosaur("Velociraptor");
20
21console.log("\nFunkcja działa przed deklaracją dzięki hoisting!");
22
23// ===========================================
24// 2. HOISTING FUNKCJI
25// ===========================================
26console.log("\n=== 2. Hoisting funkcji ===");
27
28// Deklaracje funkcji są hoistowane
29feedDinosaur("Triceratops"); // Działa
30
31function feedDinosaur(dino) {
32  console.log(`Karmienie: ${dino}`);
33}
34
35// Wyrażenia funkcyjne NIE są hoistowane
36// feedDino2("Stegosaurus"); // ERROR!
37
38const feedDino2 = function(dino) {
39  console.log(`Karmienie: ${dino}`);
40};
41
42feedDino2("Stegosaurus"); // Teraz działa
43
44// Arrow functions też NIE są hoistowane
45// feedDino3("Brachiosaurus"); // ERROR!
46
47const feedDino3 = (dino) => {
48  console.log(`Karmienie: ${dino}`);
49};
50
51feedDino3("Brachiosaurus"); // Teraz działa
52
53// ===========================================
54// 3. HOISTING VAR
55// ===========================================
56console.log("\n=== 3. Hoisting zmiennych var ===");
57
58console.log("\nPrzed deklaracją:");
59console.log("dinoName:", typeof dinoName); // undefined (nie error!)
60
61var dinoName = "T-Rex";
62
63console.log("Po deklaracji:");
64console.log("dinoName:", dinoName); // T-Rex
65
66// Tak naprawdę JavaScript interpretuje to jako:
67// var dinoName; // Deklaracja wyniesiona na górę
68// console.log(dinoName); // undefined
69// dinoName = "T-Rex"; // Przypisanie
70
71// ===========================================
72// 4. HOISTING LET I CONST
73// ===========================================
74console.log("\n=== 4. Hoisting let i const ===");
75
76// let i const też są hoistowane, ale...
77// znajdują się w "Temporal Dead Zone" (TDZ)
78
79console.log("\nPróba użycia przed deklaracją:");
80
81// console.log(dinoSpecies); // ReferenceError: Cannot access before initialization
82// console.log(parkName); // ReferenceError: Cannot access before initialization
83
84let dinoSpecies = "Velociraptor";
85const parkName = "Jurassic Park";
86
87console.log("Po deklaracji:");
88console.log("dinoSpecies:", dinoSpecies);
89console.log("parkName:", parkName);
90
91// ===========================================
92// 5. TEMPORAL DEAD ZONE (TDZ)
93// ===========================================
94console.log("\n=== 5. Temporal Dead Zone (TDZ) ===");
95
96function demonstrateTDZ() {
97  console.log("\nWewnątrz funkcji - przed deklaracją:");
98
99  // Ta strefa to TDZ dla 'dinosaur'
100  // console.log(dinosaur); // ERROR w TDZ
101
102  let dinosaur = "Triceratops";
103
104  console.log("Po deklaracji:", dinosaur); // Działa
105}
106
107demonstrateTDZ();
108
109// ===========================================
110// 6. RÓŻNICE: var vs let vs const
111// ===========================================
112console.log("\n=== 6. Porównanie: var vs let vs const ===");
113
114function compareHoisting() {
115  console.log("\nTest hoisting:");
116
117  // var - hoistowany z wartością undefined
118  console.log("var dinosaur1:", typeof dinosaur1); // undefined
119  var dinosaur1 = "T-Rex";
120  console.log("var dinosaur1:", dinosaur1); // T-Rex
121
122  // let - hoistowany, ale w TDZ
123  // console.log("let dinosaur2:", dinosaur2); // ERROR: TDZ
124  let dinosaur2 = "Velociraptor";
125  console.log("let dinosaur2:", dinosaur2); // Velociraptor
126
127  // const - hoistowany, ale w TDZ
128  // console.log("const dinosaur3:", dinosaur3); // ERROR: TDZ
129  const dinosaur3 = "Triceratops";
130  console.log("const dinosaur3:", dinosaur3); // Triceratops
131}
132
133compareHoisting();
134
135// ===========================================
136// 7. HOISTING W BLOKACH
137// ===========================================
138console.log("\n=== 7. Hoisting w blokach ===");
139
140function blockHoisting() {
141  console.log("\nPrzed blokiem:");
142  console.log("globalVar:", typeof globalVar); // undefined
143  // console.log("blockLet:", typeof blockLet); // ERROR
144
145  var globalVar = "Dostępny wszędzie";
146
147  if (true) {
148    console.log("\nW bloku:");
149    let blockLet = "Tylko w bloku";
150    const blockConst = "Tylko w bloku (const)";
151
152    console.log("blockLet:", blockLet);
153    console.log("blockConst:", blockConst);
154    console.log("globalVar:", globalVar);
155  }
156
157  console.log("\nPo bloku:");
158  console.log("globalVar:", globalVar); // Działa
159  // console.log("blockLet:", blockLet); // ERROR
160  // console.log("blockConst:", blockConst); // ERROR
161}
162
163blockHoisting();
164
165// ===========================================
166// 8. HOISTING W PĘTLACH
167// ===========================================
168console.log("\n=== 8. Hoisting w pętlach ===");
169
170console.log("\nPętla z var:");
171for (var i = 0; i < 3; i++) {
172  console.log(`  Iteracja ${i + 1}`);
173}
174console.log("Po pętli - i:", i); // Działa (i = 3)
175
176console.log("\nPętla z let:");
177for (let j = 0; j < 3; j++) {
178  console.log(`  Iteracja ${j + 1}`);
179}
180// console.log("Po pętli - j:", j); // ERROR: j is not defined
181
182// ===========================================
183// 9. PRAKTYCZNE PROBLEMY Z HOISTING
184// ===========================================
185console.log("\n=== 9. Typowe pułapki ===");
186
187// Problem 1: Nadpisywanie funkcji
188function problemExample1() {
189  console.log("\nProblem 1: Nadpisywanie");
190
191  function greet() {
192    console.log("  Pierwsze powitanie");
193  }
194
195  greet(); // Która funkcja zostanie wywołana?
196
197  function greet() {
198    console.log("  Drugie powitanie (to zostanie użyte!)");
199  }
200
201  // JavaScript "wynosi" obie deklaracje,
202  // ale druga nadpisuje pierwszą
203}
204
205problemExample1();
206
207// Problem 2: var w pętli z setTimeout
208console.log("\nProblem 2: var w pętli z setTimeout");
209
210console.log("Z var (wszystkie pokażą 3):");
211for (var k = 0; k < 3; k++) {
212  setTimeout(function() {
213    console.log(`  var k = ${k}`);
214  }, 100);
215}
216
217setTimeout(() => {
218  console.log("\nZ let (pokażą 0, 1, 2):");
219  for (let m = 0; m < 3; m++) {
220    setTimeout(function() {
221      console.log(`  let m = ${m}`);
222    }, 100);
223  }
224}, 200);
225
226// ===========================================
227// 10. BEST PRACTICES
228// ===========================================
229console.log("\n=== 10. Best practices ===");
230
231// DOBRE PRAKTYKI:
232
233// 1. Deklaruj zmienne na początku zasięgu
234function goodPractice1() {
235  const parkName = "Jurassic Park";
236  let dinosaurCount = 0;
237  let totalVisitors = 0;
238
239  dinosaurCount = 15;
240  totalVisitors = 1000;
241
242  console.log(`\n${parkName}: ${dinosaurCount} dinozaurów, ${totalVisitors} gości`);
243}
244
245goodPractice1();
246
247// 2. Używaj const domyślnie, let gdy potrzeba
248const SECURITY_LEVEL = "HIGH";
249let currentStatus = "OPERATIONAL";
250
251console.log(`Zabezpieczenia: ${SECURITY_LEVEL}`);
252console.log(`Status: ${currentStatus}`);
253
254// 3. Unikaj var
255// var oldWay = "Nie używaj tego!"; // ZŁE
256
257// 4. Deklaruj funkcje przed użyciem (czytelność)
258function wellOrganized() {
259  // Deklaracje na początku
260  const MAX_CAPACITY = 50;
261  let currentCapacity = 0;
262
263  // Funkcje pomocnicze
264  function addDinosaur(name) {
265    if (currentCapacity < MAX_CAPACITY) {
266      currentCapacity++;
267      console.log(`  Dodano ${name}. Pojemność: ${currentCapacity}/${MAX_CAPACITY}`);
268    } else {
269      console.log(`  Brak miejsca dla ${name}`);
270    }
271  }
272
273  // Główna logika
274  console.log("\nSystem zarządzania pojemnością:");
275  addDinosaur("T-Rex");
276  addDinosaur("Velociraptor");
277  addDinosaur("Triceratops");
278}
279
280wellOrganized();
281
282// ===========================================
283// 11. PRZYKŁAD KOMPLEKSOWY
284// ===========================================
285console.log("\n=== 11. System monitoringu - przykład kompleksowy ===");
286
287function createMonitoringSystem() {
288  // Deklaracje na początku (best practice)
289  const systemName = "Dino Monitor v1.0";
290  let isActive = false;
291  let alertCount = 0;
292
293  // Funkcje pomocnicze
294  function activate() {
295    isActive = true;
296    console.log(`\n${systemName} aktywowany`);
297  }
298
299  function deactivate() {
300    isActive = false;
301    console.log(`${systemName} dezaktywowany`);
302  }
303
304  function sendAlert(message) {
305    if (isActive) {
306      alertCount++;
307      console.log(`Alert #${alertCount}: ${message}`);
308    } else {
309      console.log("System nieaktywny - alert pominięty");
310    }
311  }
312
313  function getStats() {
314    console.log(`\nStatystyki ${systemName}:`);
315    console.log(`   Status: ${isActive ? "Aktywny" : "Nieaktywny"}`);
316    console.log(`   Wysłane alerty: ${alertCount}`);
317  }
318
319  // Główny interfejs
320  return {
321    start: activate,
322    stop: deactivate,
323    alert: sendAlert,
324    stats: getStats
325  };
326}
327
328const monitor = createMonitoringSystem();
329
330monitor.alert("Test przed aktywacją"); // Pominięty
331monitor.start();
332monitor.alert("T-Rex zbliża się do ogrodzenia");
333monitor.alert("Poziom zabezpieczeń krytyczny");
334monitor.stats();
335monitor.stop();
336monitor.alert("Test po dezaktywacji"); // Pominięty
337monitor.stats();
338
339console.log("\nGratulacje! Opanowałeś hoisting w JavaScript!");

Widzisz błąd w tej lekcji?

Zadania praktyczne w grze

  • Edytor kodu

    Zadeklaruj globalną stałą parkName = 'Jurassic Park'. Napisz funkcję checkEnclosure(), która tworzy lokalną stałą enclosureName = 'Zone A', a w bloku if (true) tworzy przez let zmienną alarmLevel = 5 i wyświetla w konsoli parkName, enclosureName i alarmLevel. Funkcja zwraca enclosureName. Wywołaj checkEnclosure(). Poza funkcją zmiennych enclosureName i alarmLevel ma nie być.

Przydatne artykuły