Przejdź do treści

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 siedzi let, const albo class. var bloki 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); // 1

Dwa 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 funkcji

Function 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).

← Wróć do notatek