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ą Zakres blokowy: Z wprowadzeniem |
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.