Miniatura artykułu

Scope i Hoisting

11 minut

Skopiuj link

Data publikacji: 12/29/2023, 10:00:35

Ostatnia aktualizacja: 4/19/2024

Wstęp

W dzisiejszym artykule opiszę kilka kluczowych zagadnień związanych z zakresami i zmiennymi w JavaScript. Odpowiem między innymi na pytania:

  • czym właściwie jest zakres (ang. scope)?

  • jakie rodzaje zakresów rozróżniamy?

  • czym jest wynoszenie zmiennych (ang. hoisting)?

Poruszę też temat ich wzajemnego oddziaływania.

Niektórym może się wydawać, że temat jest trywialny i niewarty poświęcenia mu uwagi przy nowoczesnym developmencie, ale zapewniam Cię że pozyskanie wiedzy na temat tego, jak działa język programowania, z którego korzystasz zaprocentuje w przyszłości.

Co to jest zakres?

Najprostszym sposobem, aby odpowiedzieć na to pytanie będzie wyobrażenie sobie zakresu, jako otaczającego środowiska lub przestrzeni nazw (kontekstu), w którym zmienne i funkcje zgłaszają swoje istnienie (są deklarowane) i do których mają dostęp. Rozróżniamy kilka rodzajów zakresów (opiszę je w kolejnej sekcji tego artykułu). Zakres określa, gdzie zmienna jest dostępna do odczytu i modyfikacji w kodzie.

Załóżmy, że mamy zmienną globalną name oraz funkcję displayName, która ma za zadanie wyświetlić tę zmienną.

Czy funkcja displayName będzie miała dostęp do zmiennej name? W momencie odczytywania kodu, interpreter JavaScriptu sprawdza, czy zmienna name znajduje się w lokalnym zakresie funkcji (czyli między jej klamrami { }). Jeśli jej tam nie znajdzie, interpreter szuka dalej w zakresie wyższym, aż dojdzie do zakresu globalnego. Odpowiadając na pytanie czy funkcja będzie miała dostęp? Owszem, tak się stanie i w konsoli (oczywiście po wywołaniu funkcji) zobaczymy przypisane wcześniej imię.

Teraz, rozważmy sytuację, gdy w naszej funkcji również zadeklarowana jest zmienna o tej samej nazwie name. Czy to możliwe? Zdecydowanie tak, i to jest przykład przysłonięcia zmiennych. Gdy w funkcji deklarujemy zmienną o nazwie już istniejącej w wyższym zakresie, nowa deklaracja "przysłania" tę z zakresu wyższego, co oznacza, że odwołania do tej nazwy wewnątrz funkcji będą dotyczyć nowo zadeklarowanej zmiennej, a nie tej z zakresu globalnego.

Przysłonięcie

Wiemy już, że jest możliwe utworzenie zmiennej o takiej samej nazwie w innych zakresach, ale która z nich zostanie wtedy odczytana w funkcji displayAnotherName? Sprawdźmy 👀

Zgodnie z zasadą przeszukiwania, która opisałem powyżej, najpierw zostanie sprawdzony zakres lokalny funkcji, tym razem interpreter napotka lokalną zmienną name o wartości "Katarzyna" i to właśnie ta wartość przysłoni zmienną z zakresu globalnego, a zjawisko to nazywamy przysłonięciem (ang. shadowing). W klasach często stosuje się tę samą nazwę dla metody lub właściwości, co pomaga unikać bezpośredniego dostępu do zmiennych globalnych i sprzyja enkapsulacji.

Warto jednak w tym miejscu wspomnieć o potencjalnych konsekwencjach, jak np. niezamierzone nadpisanie wartości zmiennej, co może prowadzić do błędów lub nieoczekiwanego zachowania w kodzie. Oto przykład:

Na początku kodu deklarujemy zmienną globalną counter i przypisujemy jej wartość 1. Ta zmienna jest dostępna w całym zakresie globalnym. W funkcji incrementCounter, deklarujemy nową zmienną o nazwie counter, która jest lokalna dla tej funkcji. Ta nowa zmienna "przysłoni" zmienną globalną counter. W funkcji zwiększamy wartość lokalnego counter o 1, co skutkuje wartością 11. Następnie używamy console.log wewnątrz funkcji, aby wyświetlić tę wartość. W tym kontekście console.log odnosi się do lokalnej zmiennej counter.

Gdy wywołujemy funkcję incrementCounter, zmodyfikowana zostaje tylko lokalna zmienna counter, a zmienna globalna pozostaje niezmieniona. Po wywołaniu funkcji używamy console.log w zakresie globalnym, aby wyświetlić wartość globalnego counter. W tym miejscu console.log odnosi się do zmiennej globalnej counter, która nie została zmodyfikowana przez funkcję. Dlatego wynik w konsoli to 1, a nie 11.

Rodzaje zakresów

Postanowiłem przedstawić rodzaje zakresów w tabeli (gdybyś chciał odświeżyć wiedzę z tego tematu możesz powrócić do tego miejsca). To na co warto zwrócić uwagę to, że istnieją trzy rodzaje zakresów: globalny, lokalny oraz leksykalny, ale jeden z nich (lokalny) dzieli się też na zakres funkcyjny oraz blokowy.

Zakres

Opis

Globalny

Z zakresem globalnym mamy do czynienia wtedy, kiedy zmienna lub funkcja jest zadeklarowana poza jakimkolwiek blokiem kodu, jest ona dostępna wszędzie w kodzie. Takie zmienne lub funkcje są określane jako globalne. Stosowane do przechowywania informacji, które mają być dostępne w całej aplikacji, lecz pamiętaj że nadużywanie zmiennych globalnych może prowadzić do konfliktów nazw i trudności w zarządzaniu kodem.

Lokalny

Zakres lokalny odnosi się do zmiennych i funkcji zadeklarowanych wewnątrz innej funkcji. Te zmienne są dostępne tylko w obrębie tej funkcji. Dzielimy go na:

Zakres funkcyjny: zmienne zadeklarowane wewnątrz funkcji za pomocą var są dostępne w całej funkcji, nawet jeśli zostały zadeklarowane w konkretnym bloku, jak na przykład w instrukcji warunkowej czy pętli. Oznacza to, że zmienna var zadeklarowana wewnątrz bloku (np. wewnątrz klamer { } instrukcji warunkowych if czy pętli for) "wycieka" do całego zakresu funkcji.

Zakres blokowy: Z wprowadzeniem let i const w ES6, zmienne mogą być również zadeklarowane w obrębie konkretnych bloków (np. if czy for) i są dostępne tylko w tych blokach. Zmienne te nie są dostępne poza blokiem, w którym zostały zadeklarowane, co pozwala na lepsze zarządzanie i kontrolę ich zakresu.

Leksykalny

Zakres leksykalny odnosi się do dostępności zmiennych w oparciu o ich umiejscowienie w zagnieżdżonych funkcjach. Wewnętrzna funkcja ma dostęp do zmiennych zadeklarowanych w funkcjach zewnętrznych. Jest to podstawa dla domknięć (closures) w JavaScript, umożliwiając funkcjom wewnętrznym dostęp do zmiennych funkcji zewnętrznych. Bardzo zachęcam Cię do artykułu dotyczącego domknięć, który został perfekcyjnie i klarownie przedstawiony przez innego twórcę tego bloga.

Każdy z tych zakresów ma swoje zastosowania i pomaga w organizacji kodu oraz zarządzaniu widocznością i żywotnością zmiennych w aplikacjach. Zrozumienie tych zakresów jest kluczowe dla efektywnego programowania, utrzymania kodu czytelnym i modularnym, a także unikania błędów związanych z niewłaściwym zarządzaniem zmiennymi.

Wynoszenie zmiennych (hoisting)

Hoisting, znany również pod nazwą: wynoszenie zmiennych, to mechanizm w JS, gdzie deklaracje zmiennych oraz funkcji są przenoszone na początek ich zakresu przed wykonaniem kodu. To zachowanie wpływa na sposób, w jaki kod jest interpretowany przez interpreter JavaScript. Aby, lepiej to zrozumieć, przedstawię Ci zachowanie hoistingu dla poszczególnych deklaracji:

Hoisting zmiennych:

var

Dla zmiennych zadeklarowanych za pomocą słowa kluczowego var, hoisting powoduje, że są one znane i dostępne w zakresie jako undefined przed ich rzeczywistą deklaracją w kodzie. Spójrz na kod:

W tym przykładzie, deklaracja zmiennej exampleVar jest wynoszona (hoistowana) na początek zakresu, ale jej inicjalizacja pozostaje na miejscu. Dlatego pierwsze wywołanie console.log daje wynik undefined.

let i const

Zmienne zadeklarowane za pomocą słów kluczowych let i const również podlegają hoistingowi, ale pozostają w tzw. martwej strefie czasowej (ang. temporal dead zone), aż do momentu ich deklaracji. Oznacza to, że odwołanie się do nich przed ich deklaracją skutkuje błędem.

Hoisting funkcji:

deklaracja funkcji

Deklaracje funkcji (ang. function declaration) są wynoszone na początek zakresu, co oznacza, że funkcja jest dostępna przed jej deklaracją w kodzie.

W tym przypadku, cała funkcja exampleFn jest wynoszona na początek zakresu, umożliwiając jej wywołanie przed deklaracją.

wyrażenie funkcyjne

Funkcje nazwane lub inaczej wyrażenia funkcyjne (ang. functional expression) nie są poddawane hoistingowi w taki sam sposób, jak za pomocą deklaracji tradycyjnej. Jeśli funkcja jest przypisana do zmiennej, to hoisting dotyczy samej zmiennej, a nie definicji funkcji. Zatem ma też znaczenie, czy użyjesz var, let, czy const do zdefiniowania wyrażenia funkcyjnego i będzie traktowany jak hoisting zmiennych dla konkretnego słowa kluczowego.

funkcje strzałkowe

Podobnie jak funkcje nazwane, funkcje strzałkowe są poddawane hoistingu w zakresie ich zmiennej, a nie jako definicje funkcji.

Ważne jest, aby zrozumieć, że hoisting zachowuje się inaczej w zależności od sposobu deklaracji. Ta różnica ma znaczenie w kontekście dostępności i używalności funkcji w różnych częściach kodu.

Dlaczego nie zaleca się stosować var?

Jak już pewnie sam wywnioskowałeś, że var niesie ze sobą spore ryzyko potencjalnych błędów i zdecydowanie nie zaleca się z niego korzystać. Jego mało przewidywalne zachowanie w kontekście hoistingu oraz zakresów opisałem już wcześniej, ale postanowiłem wspomnieć o jeszcze jednym bardzo istotnym zachowaniu. Otóż var pozwala na ponowną deklarację tej samej zmiennej w tym samym zakresie, co moim subiektywnym zdaniem jest bardzo złe. Wystarczy, że tworzymy zmienną price, a wcześniej zapomnieliśmy, że taką samą już utworzyliśmy (w tym samym zakresie), lub co gorsza nawet nie jesteśmy świadomi, że przykładowo podczas modyfikacji pliku, jakiś inny programista nie stworzył zmiennej o tej samej nazwie, a my poprzez re-deklarację zmiennej możemy ją nieświadomie nadpisać i przy okazji nieźle namieszać 😕

Zamiast tego, zaleca się stosowanie let i const, które oferują lepszą kontrolę nad zakresami zmiennych:

let - służy do deklaracji zmiennych, które mogą być przypisane ponownie. Posiada zakres blokowy, co oznacza, że jest dostępna tylko w obrębie bloku, w którym została zadeklarowana.

const - służy do deklarowania zmiennych, których wartości nie mogą być zmieniane po inicjalizacji. Tak jak let, ma zakres blokowy.

Stosowanie let i const pomaga uniknąć niejasności i potencjalnych błędów związanych z hoistingiem oraz zarządzaniem zakresami zmiennych.

Podsumowanie

W artykule omówiłem zakresy i hoisting w JavaScript, podkreślając ich znaczenie dla pisania czystego i bardziej przewidywalnego kodu. Poznałeś różne rodzaje zakresów oraz zjawisko hoistingu. Pamiętaj, że zrozumienie tych elementów jest istotne i pozwoli Ci lepiej poznawać coraz to nowsze koncepcje, fundamenty to podstawa.

Avatar: Maciej Mikołajczak

Front-end Developer

Maciej Mikołajczak

mcj.mikolajczak@gmail.com

Podziel się na

Dodaj komentarz

Komentarze (0)

Brak komentarzy