Zrozumieć hoisting
Niektóre koncepty Javascriptu wydają się nietypowe, żeby nie powiedzieć dziwne. Dziś chciałbym pochylić się nad jednym z takich mechanizmów a mianowicie hoistingiem.
Z tego artykułu dowiesz się:
Co to jest hoisting?
Jakie są różnice pomiędzy zasięgiem dynamicznym a leksykalnym?
Co to jest leksykalizacja i tokenizacja w JS?
Czy let i const również podlegają zjawisku hoistingu?
Co to jest Temporal Dead Zone?
Javascript jest ciekawym językiem. Ma zaimplementowanych wiele konceptów, które dla osób, które pierwszy raz się z nim stykają, mogą wydawać się dziwne, może nawet nieintuicyjne. Jednym z takich mechanizmów jest hoisting.
Hoisting to proces, który polega na określaniu zasięgu w jakim ma być dostępna zmienna. W większości języków programowania zmienna dostępna jest w bloku, w którym została zadeklarowana, a ponadto może być użyta dopiero po jej deklaracji, ale w Javascript jest inaczej.
Zanim przejdziemy do omówienia hoistingu w szczegółach, warto wspomnieć o zasięgu zmiennych i funkcji.
Zasięg dynamiczny i leksykalny
Zasięg, czyli scope, to nic innego jak obszar w ramach, którego dostępne są zmienne czy też wyrażenia. W programowaniu wyróżnia się dwa rodzaje zasięgów: dynamiczne i leksykalne.
Dynamiczny zasięg nie jest najpopularniejszym konceptem w dzisiejszym świecie IT. W zasadzie nie występuje w językach opartych o język C, takich jak JS. Pierwotnie został wprowadzony w języku Lisp, ale można się z nim spotkać również w takich językach jak bash czy LaTeX. Natomiast w Perl, pomimo że język domyślnie opiera się o zasięg leksykalny, można wybrać zasięg dynamiczny.
Na czym polega dynamiczność zasięgu? W zasięgu dynamicznym program szuka odpowiedniego bloku (pętli, warunku, funkcji), w którym występuje dane wyrażenie lub zmienna, a następnie sukcesywnie przechodzi do kolejnych funkcji wywołujących i na ich podstawie określa wartość wyrażenia lub zmiennej. Innymi słowy w zasięgu dynamicznym wartość zmienia się w zależności od kolejności wywoływania funkcji.
Javascript jest językiem, który opiera swoją składnię o język C. Co prawda Brendan Eich tworząc JSa bazował na wielu różnych językach: C, Modula-2 czy Java. Samo pochodzenia może nam już powiedzieć, że koncepcja zasięgu została w nim wprowadzona podobnie jak w językach, na których bazowano podczas jego projektowania. C, Java czy Javascript wykorzystują zasięg leksykalny.
Zasięg leksykalny (nazywany również zasięgiem statycznym) opiera się o lokalizację funkcji i zmiennych w czasie kompilacji. Oznacza to, że wartość zmiennej nie zmieni się w zależności od kolejnych wywołań funkcji w czasie rzeczywistym. Zasięg leksykalny jest zdecydowanie łatwiejszy do opanowania przez programistę. Bardzo często wystarczy przeczytać fragment kodu, aby wywnioskować wartość bez uwzględniania różnych kontekstów wywołań, które mogłyby na nią wpłynąć.
Poniższy przykład pokazuje jak działa zasięg leksykalny. Zmienna x jest zależna tylko od bloku, którym została zadeklarowana. Nie ma znaczenia gdzie została wywołana funkcja, która zwraca wartość zmiennej x. Gdyby JS posiadał dynamiczne zasięgi wynikiem wywołania funkcji bar, byłaby wartość 2.
Hoisting
W języku Javascript zmienne możemy deklarować na 3 sposoby, a funkcje na 2 sposoby. Jeśli chodzi o deklarowanie zmiennych mamy do dyspozycji var, let oraz const, w przypadku funkcji możemy wykorzystać słowo kluczowe function lub zapis arrow function. Na początku skupmy się na działaniu hoistingu w przypadku słów kluczowych var oraz function.
Jak już wspomniałem hoisting polega na określaniu zasięgu, w którym dana wartość ma być dostępna. Można śmiało powiedzieć, że mechanizm ten przenosi deklaracje zmiennych na początek ich zasięgu jeszcze przed wykonaniem kodu. Sprowadza się to do sytuacji, w której zmienne będą dostępne nawet przed ich deklaracją. Spójrzmy na poniższe przykłady:
Ta sama funkcja została zapisana w dwóch różnych językach. W obu przypadkach kolejność deklaracji i odniesień do zmiennej jest taka sama. Różnica polega na tym, że Javascript (pierwszy przykład) określa zmienną jako wartość undefined
(co ważne, nie zwraca błędu), a Java zwraca błąd. Co ciekawe ten sam mechanizm działa również na funkcje zapisane za pomocą słowa kluczowego function
.
Podsumowując hoisting sprawi, że zmienna będzie dostępna i może być wykorzystana nawet przed jej zadeklarowaniem.
Zasięgi leksykalne w Javascript
Warto wspomnieć o tym, że w Javascript występują różne rodzaje zasięgów leksykalnych. Upraszczając temat możemy wyodrębnić 3 podstawowe: zasięg globalny (w przeglądarce powiązany z obiektem window), zasięg leksykalny funkcji oraz zasięg leksykalny bloku. Zasięgi blokowe zostały wprowadzone do JSa wraz z nowymi sposobami deklaracji zmiennych za pomocą let oraz const. Można powiedzieć, że zasięg funkcji jest odmianą zasięgu blokowego. Różnica polega na tym, że zmienne deklarowane za pomocą var w innych blokach (np. pętlach czy warunkach) są traktowane jako przynależne do bloku funkcji, w której zostały zadeklarowane. Natomiast zmienne deklarowane za pomocą let czy const są przynależne do dowolnego bloku, w którym zostały zadeklarowane. Spójrzmy na przykład:
let i const vs hoisting
Panuje mylne przekonanie, że na let
i const
nie działa mechanizm hoistingu. Wynika to z faktu, że JS wprowadził dodatkowy mechanizm sprawdzania odwołań do zmiennych deklarowanych w ten sposób. Jeśli odwołamy się do zmiennej zadeklarowanej za pomocą let
lub const
przed jej deklaracją otrzymamy wiele mówiący komunikat: „Cannot access variable before initialization”. Gdyby mechanizm hoistingu nie działał otrzymalibyśmy błąd, podobny do tego, gdy odwołamy się do zmiennej bez jej deklaracji. Zobaczmy przykład:
Jak widać mamy do czynienia z dwiema różnymi sytuacjami, w przypadku zmiennej b Javascript wie o jej istnieniu, mówi natomiast że nie możemy jej użyć przed jej zainicjowaniem.
Przestrzeń pomiędzy dostępnością zmiennej a odwołaniem do niej to tak zwana Temporal Dead Zone.
Kiedy odbywa się hoisting?
Javascript jest językiem interpretowanym. Języki interpretowane wykorzystują specjalne programy, tzw. interpretery do analizy kodu. Kod analizowany jest linijka po linijce, kod maszynowy, który powstaje w tym procesie, nie jest nigdzie zapisywany. Najczęściej języki interpretowane są wolniejsze niż kompilowane. Kod wykonywany jest z pamięci jako część całego procesu interpretowania kodu. Skoro już wiemy mniej więcej co się dzieje z Javascriptem, to w takim razie w którym momencie dzieje się hoisting?
Przede wszystkim trzeba zwrócić uwagę na to, że Javascript w większości przypadków uruchamiany jest w przeglądarkach. Nowoczesne przeglądarki wykorzystują technologię kompilacji JIT (Just-In-Time), która oznacza, że kod jest przekształcany do wykonywalnego kodu maszynowego, a następnie uruchamiany. Silniki Javascriptowe (takie jak V8 czy SpiderMonkey) w zasadzie łączą w sobie cechy interpreterów i kompilatorów. Kod JS przechodzi w silniku JS przez dwie fazy. Pierwsza to faza kompilacji, w czasie której wystąpi zjawisko hoistingu zmiennych. W zasadzie sprowadza się to do dwóch procesów: leksykalizacji i tokenizacji. W szczegółach, jest to nic innego jak dzielenie kodu na mniejsze tokeny, z których następnie tworzone jest tak zwane AST (Abstract Syntax Tree) dla analizowanego zasięgu.
W tym momencie, gdy silnik napotka deklarację zmiennej przydziela jej pamięć, nie modyfikuje kodu. Powstaje w ten sposób wiązanie pomiędzy zmienną a pamięcią. W trakcie przydzielania pamięci do zmiennej zostaje do niej przypisana wartość domyślna, czyli undefined
. Następnie jest ustalany zasięg na podstawie przypisań i dopiero wtedy generowany jest kod maszynowy.
Podsumowanie
Hoisting jako zjawisko jest opisany jednym zdaniem, a jak widać można o nim powiedzieć troszkę więcej. Pojęcie to zostało ukute przez programistów, aby prościej wyjaśnić cały proces alokacji pamięci dla zmiennych w czasie fazy kompilacji kodu Javascriptowego. Jak widać nic nie dzieje się bez przyczyny - a sama specyfika tego zjawiska jest bezpośrednio powiązana z tym, jak działa i gdzie wykorzystywany jest Javascript.
Komentarze (1)
tworzeniestroninternetowych_warszawa_pl
31 sierpnia 2023 o 10:31Tekst wyjaśla hoisting w języku JavaScript w sposób przejrzysty. Mechanizm ten przenosi deklaracje zmiennych i funkcji na początek ich zasięgu, co może być mylące, szczególnie dla początkujących programistów. Wysuwa się istotne kontrasty pomiędzy zasięgami dynamicznym i leksykalnym. Hoisting wzbogaca zrozumienie tego, jak kod jest kompilowany i wykonywany w JavaScript. Pomimo że hoisting jest użytecznym mechanizmem, ważne jest, aby unikać niejednoznacznych deklaracji i korzystać z niego w sposób umiejętny.