Czym jest leksykalny scope w JavaScript
Skąd silnik wie, że jedna funkcja widzi zmienne drugiej, a nie odwrotnie? W JavaScript decyduje o tym samo położenie kodu, jeszcze zanim cokolwiek się wykona. Tak działa leksykalny scope.
Pisząc kod, każdy intuicyjnie wie, że funkcja zagnieżdżona widzi zmienne tej, która ją otacza - a nie odwrotnie:
function outer() {
const a = 1;
function inner() {
console.log(a); // 1, jasne
}
inner();
}Ta intuicja jest mocniejsza niż się wydaje - to nie nawyk wzięty z konwencji pisania kodu, tylko reguła wpisana w specyfikację języka. Nazywa się leksykalny scope.
#Skąd nazwa lexical
Sam termin lexical wskazuje na moment, w którym silnik analizuje kod, zanim go wykona. To ta sama faza kompilacji, w której rejestruje też nazwy do hoistingu. Wtedy znajduje wszystkie deklaracje i ustala, co jest widoczne w którym miejscu. Scope wynika z tego, gdzie funkcja albo blok znajduje się w kodzie - nie z tego, kto ją później wywoła.
Przeciwieństwem są języki z dynamicznym scope, w których widoczność zmiennej zależy od kontekstu wywołania (tak działał wczesny Lisp). W JS możesz spojrzeć w plik i bez uruchamiania powiedzieć, do czego dana funkcja ma dostęp. Statycznie, leksykalnie.
#Co tworzy nowy scope
JS otwiera nowy scope w dwóch miejscach:
- Funkcja - każda deklaracja albo wyrażenie funkcji.
- Blok
{ ... }- jeśli w środku siedzilet,constalboclass.varbloki ignoruje i sięga do najbliższej funkcji albo do globalnego scope.
Z zagnieżdżenia powstaje łańcuch (po angielsku scope chain): wewnętrzny scope widzi ten, który go otacza, tamten swój, aż do globalnego. Silnik szuka nazwy od środka na zewnątrz - nigdy odwrotnie.
#Shadowing
Wewnętrzny scope może zadeklarować zmienną o tej samej nazwie co rodzic. To nie konflikt, to przesłonięcie - shadowing. Wewnątrz widać tę nowo zadeklarowaną, na zewnątrz nadal tę z otaczającego scope:
let x = 1;
function f() {
let x = 2;
console.log(x); // 2
}
f();
console.log(x); // 1Dwa różne miejsca w pamięci pod tą samą nazwą, każde obowiązuje w swoim scope.
#Gdzie ląduje nazwa funkcji
Drobiazg z deklaracji vs wyrażeń, który łatwo przeoczyć:
function declared() {} // nazwa "declared" idzie do scope rodzica
const x = function expr() {}; // nazwa "expr" zostaje WEWNĄTRZ funkcjiFunction declaration dodaje swoją nazwę do otaczającego scope. Wyrażenie funkcji - jeśli ma nazwę - dodaje ją tylko do swojego scope (po to, żeby funkcja mogła zawołać samą siebie po imieniu). Na zewnątrz dostępna jest wyłącznie pod nazwą zmiennej, do której została przypisana - tu pod x, nie pod expr.
#Słowo na koniec
Pytanie „gdzie silnik znajdzie zmienną o danej nazwie” rozstrzyga się raz, na podstawie tego, gdzie kod fizycznie stoi w pliku - jeszcze zanim funkcja zostanie wywołana. Ten sam fakt wraca potem przy hoistingu (silnik rejestruje nazwy w scope na tej samej fazie kompilacji) i w closure (funkcja pamięta scope, w którym powstała, nie ten, z którego ją wywołasz).