Miejsce, w którym zapisana jest funkcja, decyduje o tym, co może widzieć: zakres leksykalny w JavaScript
Zrozum, w jaki sposób JavaScript rozwiązuje nazwę zmiennej poprzez środowiska leksykalne, dlaczego miejsce wywołania nigdy nie ma znaczenia przy wyszukiwaniu, oraz jak to wpływa na obsługiwanie zdarzeń w React.
Źródła: How JavaScript Finds Variables: Scope, Lexical Environment & Scope Chain
Każdy raz, gdy JavaScript ewaluuje nazwę taką jak user, silnik musi określić, do którego powiązania odnosi się ta nazwa. Najczęstszy błąd, polegający na założeniu, że funkcja wywoływana może widzieć zmienne swojego wywołującego, występuje zarówno podczas rozmów kwalifikacyjnych, jak i w błędach w produkcji. Ten przewodnik omawia temat, przechodząc od zasięgu po środowiska leksykalne aż po rozwiązywanie problemów z identyfikatorami, przy szczególnym nacisku na różnicę między łańcuchem zasięgów a stosem wywołań, abyś mógł przewidzieć wszelkie problemy z funkcjami nawzajem zawartymi i jasno myśleć o obsługach w React.
Pytanie: jak funkcja nawzajem zawarta znajduje zmienną zewnętrzną?
Zacznijmy od trzech poziomów zawierania się:
const appName = "HealthApp";
function dashboard() {
const user = "Jashan";
function showProfile() {
const status = "Active";
console.log(appName);
console.log(user);
console.log(status);
}
showProfile();
}
dashboard();
showProfile() deklaruje tylko status, a mimo to wszystkie trzy wywołania console.log odnoszą sukces. Każda nazwa jest znajdowana w innym miejscu:
status → showProfile()
user → dashboard()
appName → global
Wyjaśnienie tego wymaga krótkiej sekwencji koncepcji:
Scope
↓
Lexical Scope
↓
Lexical Environment
↓
Scope Chain
↓
Identifier Resolution
Zakres: gdzie nazwa może być używana
Zakres to obszar kodu, w którym można odwołać się do identyfikatora: odpowiada na pytanie „skąd mogę użyć tej nazwy?”
function dashboard() {
const user = "Jashan";
console.log(user); // ✅
}
dashboard();
console.log(user); // ❌ ReferenceError
user należy do dashboard(); kod znajdujący się poza nim powoduje błąd ReferenceError. Przedstawione jako drzewo:
GLOBAL
│
└── dashboard()
└── user
To prowadzi do następującej zasady:
Inside dashboard → accessible ✅
Outside dashboard → inaccessible ❌
Dlaczego izolacja jest przydatna
Każda z tych dwóch funkcji deklaruje zmienną data:
function loadPatients() {
const data = ["Patient A"];
}
function loadAppointments() {
const data = ["Appointment A"];
}
Nigdy nie dochodzi między nimi do kolizji, ponieważ każda z nich funkcjonuje w swoim własnym zakresie funkcji. Ta izolacja sprawia, że kod jest wielokrotnie używalny i łatwiejszy do debugowania.
Cztery rodzaje zakresu
JavaScript posiada cztery główne typy zakresu:
Scope
├── Global Scope
├── Function Scope
├── Block Scope
└── Module Scope
Zakres globalny
W klasycznym skrypcie (niemodułowym) deklaracje na najwyższym poziomie należą do zakresu globalnego i są dostępne z dowolnego miejsca:
var appName = "HealthApp";
function showAppName() {
console.log(appName);
}
showAppName() znajduje appName, ponieważ ścieżka wyszukiwania kończy się w środowisku globalnym. Jeden szczegół: deklaracje var na najwyższym poziomie stają się również właściwościami obiektu globalnego (window w przeglądarkach), podczas gdy deklaracje let i const na najwyższym poziomie tak nie robią. Trzymaj stan globalny w minimalnym stopniu – powoduje on powiązanie odległego kodu i utrudnia testowanie.
Zakres funkcji
Wszystko, co jest deklarowane wewnątrz funkcji, jest lokalne dla niej:
function calculateTotal() {
const price = 100;
const tax = 20;
return price + tax;
}
console.log(price); // ❌
price i tax są zmiennymi lokalnymi, dlatego ostatni log wywołuje błąd.
var ignoruje bloki
var ma zakres najbliższej funkcji, a nie najbliższego pary nawiasów:
function example() {
if (true) {
var message = "Hello";
}
console.log(message); // ✅ Hello
}
Blok if nie zawiera zmiennej message, więc pozostaje czytelny po zakończeniu tego bloku.
Zakres bloku z let i const
Zmienne let i const są ograniczone do bloku, w którym zostały zadeklarowane:
if (true) {
const message = "Payment successful";
let status = "completed";
}
console.log(message); // ❌
console.log(status); // ❌
Oba logi nie działają poprawnie. Pętle zachowują się w ten sam sposób, a przy każdej iteracji pętli for (let ...) zmienne i otrzymuje nową wartość:
for (let i = 0; i < 3; i++) {
console.log(i);
}
console.log(i); // ❌
Należy pamiętać, że nawiasy literala obiektu opisują wartość, a nie blok, więc nie tworzą żadnego zakresu.
Zakres modułu
W modułach ES deklaracje na najwyższym poziomie są prywatne dla danego pliku:
// auth.js
const token = "secret";
export function getUser() {}
Inny moduł nie może uzyskać dostępu do zmiennej token:
// dashboard.js
console.log(token); // ❌
Tylko to, co moduł wyraźnie eksportuje, może być importowane w innym miejscu:
import { getUser } from "./auth.js";
To jest standardowy zakres w projektach React, Next.js, Vite oraz nowoczesnych projektach Node.js.
Zakres leksykalny: decyduje pozycja w kodzie źródłowym
JavaScript ma zakres leksykalny: funkcja ma dostęp do zmiennych ze wszystkich zakresów, które ją otaczają w kodzie źródłowym. Ta struktura jest ustalana podczas analizy kodu, zanim cokolwiek zostanie uruchomione.
const name = "Global";
function outer() {
const name = "Outer";
function inner() {
console.log(name);
}
inner();
}
outer();
Program wyświetla:
Outer
inner() jest zawarty wewnątrz outer() w kodzie źródłowym, więc name z outer jest najbliższy:
GLOBAL
│
└── outer()
├── name = "Outer"
│
└── inner()
Poиск name odbywa się od inner i zatrzymuje się przy outer:
inner
↓
not found locally
↓
outer
↓
name = "Outer" ✅
Zakres leksykalny jest określany przez miejsce, w którym zdefiniowana została funkcja, a nie przez to, gdzie jest ona wywoływana.
Pułapka na rozmowie kwalifikacyjnej: definicja versus wywołanie
Teraz przenieś inner() poza outer(), podczas gdy outer() nadal go wywołuje:
const name = "Global";
function outer() {
const name = "Outer";
inner();
}
function inner() {
console.log(name);
}
outer();
Częstym przypuszczeniem jest:
Outer
Rzeczywisty wynik jest następujący:
Global
inner() jest zdefiniowany na najwyższym poziomie, obok outer(), a nie w jego wnętrzu.
GLOBAL
├── name = "Global"
├── outer()
└── inner()
Dlatego ścieżka wyszukiwania trafia bezpośrednio do środowiska globalnego:
inner
↓
Global
Nie przechodzi przez outer:
inner
↓
outer ❌
To, że outer() wywołało inner(), nic nie zmienia. Miejsce wywołania kształtuje stos wywołań; miejsce definicji określa zakres leksykalny.
Środowiska leksykalne: mechanizm stojący za zakresem
Specyfikacja ECMAScript modeluje zakres za pomocą środowisk leksykalnych. Każde z nich rejestruje dwie rzeczy:
- identyfikatory związane w tym środowisku
Jako model mentalny:
Lexical Environment
│
├── Environment Record
└── Outer Environment Reference
Nowsze wersje specyfikacji łączą zewnętrzny link z Rekordem Środowiska jako pole [[OuterEnv]]; model pozostaje niezmieniony.
Rekord środowiska przechowuje powiązania
Weźmy funkcję z dwiema lokalnymi stałymi:
function dashboard() {
const user = "Jashan";
const role = "Admin";
}
Koncepcyjnie jej rekord środowiska mapuje nazwy na wartości:
dashboard Environment
user → "Jashan"
role → "Admin"
To jest model, a nie obiekt, który twój kod może sprawdzić.
Zewnętrzne odniesienie łączy środowiska
Każde środowisko wskazuje na to, które leksykalnie je otacza:
const appName = "HealthApp";
function dashboard() {
const user = "Jashan";
}
Dla dashboard() wygląda to w ten sposób:
dashboard Environment
│
├── user = "Jashan"
└── Outer
↓
Global Environment
Ten wskaźnik umożliwia wyszukiwanie poza zakresem lokalnym. Jest ustawiany podczas tworzenia funkcji, w zależności od miejsca jej definicji, dlatego lokalizacja wywołania nie ma znaczenia.
Śledzenie całego łańcucha
Z powrotem do początkowego przykładu, z połączonymi trzema logami:
const appName = "HealthApp";
function dashboard() {
const user = "Jashan";
function showProfile() {
const status = "Active";
console.log(appName, user, status);
}
showProfile();
}
dashboard();
Gdy uruchamiana jest funkcja showProfile(), jej środowiska są powiązane w ten sposób:
SHOWPROFILE ENVIRONMENT
│
├── status = "Active"
└── Outer
↓
DASHBOARD ENVIRONMENT
│
├── user = "Jashan"
└── Outer
↓
GLOBAL ENVIRONMENT
│
├── appName = "HealthApp"
└── Outer
↓
null
Zewnętrzy odwołanie środowiska globalnego wynosi null, co kończy łańcuch.
Łańcuch zakresów i rozwiązywanie identyfikatorów
Łańcuch zakresów to ścieżka, którą podąża silnik w celu znalezienia identyfikatora: sprawdza bieżące środowisko, następnie podąża za zewnętrznym odwołaniem, aż zostanie znaleziona nazwa lub łańcuch się zakończy.
Current Environment
↓
Outer Environment
↓
Outer Environment
↓
Global
↓
null
Znajdowanie powiązania wzdłuż tej ścieżki nazywa się rozwiązywaniem identyfikatorów. Weźmy trzy takie wyszukiwania:
console.log(status);
console.log(user);
console.log(appName);
Rozwiązywanie problemu z status
Znaleziono bezpośrednio w środowisku lokalnym:
showProfile
↓
status found ✅
Rozwiązywanie problemu z user
Nie ma go lokalnie, znaleziono na poziom wyżej:
showProfile ❌
↓
dashboard
↓
user found ✅
Rozwiązywanie problemu z appName
Nie ma go w żadnej z funkcji, znaleziono w środowisku globalnym:
showProfile ❌
↓
dashboard ❌
↓
Global
↓
appName found ✅
We wszystkich przypadkach wyszukiwanie zatrzymuje się przy pierwszym dopasowaniu. Aby lepiej poznać sam algorytm wyszukiwania, sprawdź jak łańcuch zakresów w JavaScript faktycznie rozwiązuje zmienne.
Gdy rozwiązanie nie udaje się
Rozważmy funkcję, która próbuje odczytać nazwę, której nikt nie deklarował:
function showProfile() {
console.log(age);
}
i wywołanie tej funkcji:
showProfile();
Jeśli age nie istnieje w żadnym z dostępnych środowisk, wyszukiwanie się kończy:
showProfile
↓
Global
↓
null
Motor nie może rozwiązać nazwy i rzuca błędem:
ReferenceError: age is not defined
Różni się to od zmiennej zawierającej undefined lub związku typu let, który jeszcze nie został inicjalizowany (strefa martwa tymczasowa). Te trzy stany łatwo się mylić:
undefined
uninitialized
undeclared
Trzy zasady obejmujące większość przypadków
Szukanie odbywa się tylko na zewnątrz
Inner
↓
Outer
↓
Global
Kod wewnętrzny może odczytywać zmienne zewnętrzne. Kod zewnętrzny nie może dotrzeć do zagnieżdżonego zakresu, aby odczytać zmienne deklarowane tylko tam.
Zwycięża najbliższy związek
Gdy ta sama nazwa istnieje na kilku poziomach, używany jest ten najbliższy:
const user = "Global User";
function dashboard() {
const user = "Dashboard User";
function showProfile() {
console.log(user);
}
showProfile();
}
Wynik:
Dashboard User
Poszukiwanie kończy się w dashboard, po jednym kroku wyżej:
showProfile ❌
↓
dashboard ✅
↓
STOP
Globalna zmienna user nigdy nie jest sprawdzana. Taka wewnętrzna deklaracja, która ukrywa zmienną zewnętrzną, nazywana jest zacienianiem.
Nic nie jest kopiowane do funkcji wewnętrznych
Kuszącym, ale błędnym obrazem jest to, że funkcje wewnętrzne otrzymują kopie zmiennych zewnętrznych:
showProfile
├── copied user
└── copied appName
Prawidłowy obraz to łańcuch referencji:
showProfile
↓
dashboard
↓
Global
Czytanie wartości przez link oznacza, że funkcja wewnętrzna zawsze widzi aktualną wartość zmiennych zewnętrznych, co umożliwia działanie zamknięć funkcji.
Łańcuch zakresów versus stos wywołań
Oto ponownie przykład pułapki:
const name = "Global";
function outer() {
const name = "Outer";
inner();
}
function inner() {
console.log(name);
}
outer();
Gdy wykonywana jest funkcja inner(), stos wywołań wygląda w ten sposób:
┌──────────────────┐
│ inner FEC │ ← Current
├──────────────────┤
│ outer FEC │
├──────────────────┤
│ Global EC │
└──────────────────┘
Funkcja outer() znajduje się oczywiście na stosie, ponieważ wywołała inner(). Mimo to inner() nadal wyświetla:
Global
ponieważ wyszukiwanie wartości odbywa się według łańcucha zakresów, a łańcuch funkcji inner prowadzi bezpośrednio do środowiska globalnego. Te dwie struktury odpowiadają na różne pytania:
- Stos wywołań śledzi konteksty wykonawcze, które są obecnie aktywne; łańcuch zakresu rozwiązuje identyfikatory.
- Stos wywołań jest uporządkowany według tego, która funkcja wywołuje którą; łańcuch zakresu jest kształtowany przez sposób użycia nawiasów w kodzie źródłowym.
- W stosie wywołań obowiązuje zasada „ostatnio dodane, pierwsze usunięte”; łańcuch zakresu podąża za powiązaniami ze zewnętrznym środowiskiem.
- Stos wywołań odpowiada na pytanie „co jest aktualnie w trakcie działania?”, natomiast łańcuch zakresu odpowiada na pytanie „gdzie znajduje się ten identyfikator?”
Konteksty wykonawcze i środowiska leksykalne
Kontekst wykonawczy
Kontekst wykonawczy przechowuje stan niezbędny podczas działania kodu. Istnieją dwa główne rodzaje:
Global Execution Context
Function Execution Context
Środowisko leksykalne
Środowisko leksykalne przechowuje powiązania identyfikatorów oraz odnośnik do jego zewnętrznego środowiska. Kontekst wykonawczy odwołuje się do swojego środowiska mniej więcej w ten sposób:
Execution Context
│
└── environment information
↓
Lexical Environment
│
├── bindings
└── outer environment
Zatem nie są to to samo:
Execution Context ≠ Lexical Environment
Kontekst wykonywania wyjaśnia, jak uruchamia się kod; środowisko leksykalne wyjaśnia, jak znajdują się nazwy.
Zakres leksykalny w komponentach React
Kod React stale polega na zakresie leksykalnym. Rozważmy obsługę zdarzeń zdefiniowaną wewnątrz komponentu:
function UserDashboard({ userId }: { userId: string }) {
const role = "Admin";
function handleProfileClick() {
console.log(userId);
console.log(role);
}
return (
<button onClick={handleProfileClick}>
View Profile
</button>
);
}
handleProfileClick() nie deklaruje żadnej z tych nazw:
userId
role
Mimo to je używa, ponieważ jest napisana wewnątrz UserDashboard:
handleProfileClick
↓
UserDashboard
├── userId
└── role
Ten sam mechanizm dotyczy propów, stanu, funkcji zwrotnych, timerów, obietnic i hooków. Każde renderowanie tworzy nowe środowisko z wartościami tego renderowania, a funkcje zwrotne zdefiniowane w jego trakcie odnoszą się dokładnie do tych wartości. To jest podstawa zamknięć, a także starych zamknięć, w których funkcja zwrotna z wcześniejszego renderowania nadal odczytuje stary stan.
Powszechne błędne przekonania
"Wyszukiwanie odbywa się zgodnie z miejscem wywołania"
To nieprawda. Decydujące jest miejsce definicji:
Where function is called ❌
Where function is defined ✅
">Stos wywołań i łańcuch zakresu to to samo"
Służą one różnym celom:
Call Stack → execution
Scope Chain → identifier lookup
">Kod zewnętrzny może odczytywać zmienne wewnętrzne"
Wyszukiwanie jest jednokierunkowe:
Inner → Outer → Global ✅
Outer → Inner ❌
"var uwzględnia bloki"
To nieprawda; tylko let i const to robią:
var → function-scoped
let/const → block-scoped
">Środowisko leksykalne to zwykły obiekt"
Jest to koncepcja z specyfikacji; silniki implementują je swobodnie, a twój kod nie może do nich uzyskać dostępu.
Model mentalny do zapamiętania
Temat można streścić w serii pytań:
Scope
↓
Where can I use this variable?
↓
Lexical Scope
↓
Where was this function defined?
Lexical Environment
↓
What variables exist here?
Outer Environment
↓
Where do I look next?
Scope Chain
↓
Current → Outer → Global
Identifier Resolution
↓
Find the matching variable
Główne wnioski
- Zakres określa, gdzie można używać nazwy; JavaScript posiada zakres globalny, funkcji, bloku i modułu, przy czym
varjest ograniczony do funkcji, alet/const– do bloków. - Zakres jest leksykalny: zewnętrzne środowisko funkcji jest ustalone w miejscu jej definicji, więc wywołanie jej z innego miejsca nigdy nie zmienia tego, co może ona widzieć.
- Rozwiązywanie identyfikatorów odbywa się od bieżącego środowiska na zewnątrz, zatrzymuje się przy pierwszym dopasowaniu (zaciemnianiu) i wywołuje błąd
ReferenceError, jeśli dotrze do końca. - Funkcje wewnętrzne przechowują odniesienia do zewnętrznych środowisk, a nie ich kopie, co stanowi podstawę zamknięć oraz zastarzałych zamknięć w React.
Literatura pokrewna
- Jak łańcuch horyzontu JavaScript faktycznie rozwiązuje zmienne — Wyjaśnia mechanizm łańcucha horyzontu leksykalnego odpowiadający za wyszukiwanie zmiennych, dlaczego użycie var powoduje błędy w pętlach oraz jak z tego naturalnie wynikają zamknięcia.
- Poza rozmiarem pakietu: Znajdowanie tego, co faktycznie spowalnia twoją aplikację webową — Dlaczego redukcja liczby kilobajtów rzadko naprawia wolną aplikację oraz jak śledzić rzeczywisty czas oczekiwania na serwerach, w procesach typu waterfall, podczas hydratacji strony, a także ze względu na skrypty i obrazy od dostawców zewnętrznych.