Wo eine Funktion geschrieben wird, bestimmt, was sie wahrnimmt: Der lexikalische Umfang in JavaScript
Verstehen Sie, wie JavaScript einen Variablennamen über lexikalische Umgebungen auflöst, warum der Aufrufort für die Suche nie eine Rolle spielt und wie dies auf React-Handler übertragen wird.
Quellen: How JavaScript Finds Variables: Scope, Lexical Environment & Scope Chain
Jedes Mal, wenn JavaScript einen Namen wie user auswertet, muss der Interpreter entscheiden, auf welche Bindung er sich bezieht. Der häufigste Fehler – der Annahme, dass eine aufgerufene Funktion die Variablen ihrer aufrufenden Funktion sehen kann – tritt sowohl in Interviews als auch bei Fehlern in der Produktion auf. Diese Anleitung geht von dem Scope über lexikalische Umgebungen bis zur Identifikatorlösung und legt besonderen Wert auf den Unterschied zwischen Scope-Chain und Call-Stack, damit Sie jedes Problem mit verschachtelten Funktionen vorhersagen und klar über React-Handler nachdenken können.
Die Frage: Wie findet eine verschachtelte Funktion externe Variablen?
Begonnen wird mit drei verschachtelten Ebenen:
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() deklariert nur status, dennoch gelingen alle drei console.log-Aufrufe. Jeder Name wird an einer anderen Stelle gefunden:
status → showProfile()
user → dashboard()
appName → global
Das zu erklären erfordert eine kurze Abfolge von Konzepten:
Scope
↓
Lexical Scope
↓
Lexical Environment
↓
Scope Chain
↓
Identifier Resolution
Gültigkeitsbereich: Wo ein Name verwendet werden kann
Der Gültigkeitsbereich ist der Codeabschnitt, in dem ein Identifikator aufgerufen werden kann – er beantwortet die Frage „Von wo aus kann ich diesen Namen verwenden?“
function dashboard() {
const user = "Jashan";
console.log(user); // ✅
}
dashboard();
console.log(user); // ❌ ReferenceError
user gehört zu dashboard(); Code außerhalb führt zu einem ReferenceError. Als Baum dargestellt:
GLOBAL
│
└── dashboard()
└── user
Das führt zur Regel:
Inside dashboard → accessible ✅
Outside dashboard → inaccessible ❌
Warum Isolation sinnvoll ist
Jede dieser beiden Funktionen deklariert eine data-Variable:
function loadPatients() {
const data = ["Patient A"];
}
function loadAppointments() {
const data = ["Appointment A"];
}
Es kommt niemals zu Kollisionen, da jede Funktion in ihrem eigenen Gültigkeitsbereich existiert. Diese Isolation sorgt dafür, dass der Code wiederverwendbar und leichter zu debuggen ist.
Die vier Arten von Gültigkeitsbereichen
JavaScript verfügt über vier Haupttypen von Gültigkeitsbereichen:
Scope
├── Global Scope
├── Function Scope
├── Block Scope
└── Module Scope
Globaler Gültigkeitsbereich
In einem klassischen (nicht-modularen) Skript gehören die Deklarationen auf höchster Ebene zum globalen Scope und sind von überall aus erreichbar:
var appName = "HealthApp";
function showAppName() {
console.log(appName);
}
showAppName() findet appName, weil sein Suchpfad beim globalen Umfeld endet. Ein Detail: var-Deklarationen auf höchster Ebene werden ebenfalls zu Eigenschaften des globalen Objekts (window in Browsern), während let- und const-Deklarationen dies nicht tun. Halten Sie den globalen Zustand minimal – er verknüpft entfernten Code miteinander und erschwert das Testen.
Funktionsscope
Alles, was innerhalb einer Funktion deklariert wird, ist dort lokal:
function calculateTotal() {
const price = 100;
const tax = 20;
return price + tax;
}
console.log(price); // ❌
price und tax sind lokal, weshalb die anschließende Ausgabe einen Fehler auslöst.
var ignoriert Blöcke
var ist auf die nächstgelegene Funktion beschränkt, nicht auf das nächste Paar Klammern:
function example() {
if (true) {
var message = "Hello";
}
console.log(message); // ✅ Hello
}
Der if-Block enthält keine message, sodass diese nach dem Block weiterhin lesbar ist.
Blockumfang mit let und const
let und const sind auf den Block beschränkt, in dem sie deklariert werden:
if (true) {
const message = "Payment successful";
let status = "completed";
}
console.log(message); // ❌
console.log(status); // ❌
Sowohl die Protokollierungen als auch die Schleifen versagen. Schleifen verhalten sich auf dieselbe Weise, und jede Iteration einer for (let ...)-Schleife erhält eine neue Bindung für i:
for (let i = 0; i < 3; i++) {
console.log(i);
}
console.log(i); // ❌
Beachten Sie, dass die Klammern eines Objektliterals einen Wert beschreiben und keinen Block, weshalb sie keinen Umfang erzeugen.
Modulumfang
In ES-Modulen sind Deklarationen auf höchster Ebene für die Datei selbst privat:
// auth.js
const token = "secret";
export function getUser() {}
Ein anderes Modul kann token nicht sehen:
// dashboard.js
console.log(token); // ❌
Nur das, was ein Modul explizit exportiert, kann an anderer Stelle importiert werden:
import { getUser } from "./auth.js";
Dies ist der übliche Gültigkeitsbereich in React, Next.js, Vite und modernen Node.js-Projekten.
Lexikalischer Gültigkeitsbereich: Die Position im Quellcode entscheidet
JavaScript verfügt über einen lexikalischen Gültigkeitsbereich: Eine Funktion kann die Variablen aller in ihrem Quelltext liegenden übergeordneten Gültigkeitsbereiche einsehen. Diese Struktur wird bereits bei der Parsing des Codes festgelegt, bevor etwas ausgeführt wird.
const name = "Global";
function outer() {
const name = "Outer";
function inner() {
console.log(name);
}
inner();
}
outer();
Das Programm gibt aus:
Outer
inner() ist im Quelltext innerhalb von outer() eingebettet, daher ist name aus outer der nächstgelegene:
GLOBAL
│
└── outer()
├── name = "Outer"
│
└── inner()
Die Suche nach name erfolgt von inner ausgehend nach außen und stoppt bei outer:
inner
↓
not found locally
↓
outer
↓
name = "Outer" ✅
Der lexikalische Gültigkeitsbereich wird durch den Ort bestimmt, an dem eine Funktion definiert wird – niemals durch den Ort, an dem sie aufgerufen wird.
Die Interview-Falle: Definition versus Aufruf
Nun nehmen Sie inner() aus outer() heraus, solange outer() es noch aufruft:
const name = "Global";
function outer() {
const name = "Outer";
inner();
}
function inner() {
console.log(name);
}
outer();
Eine häufige Vermutung ist:
Outer
Die tatsächliche Ausgabe ist:
Global
inner() wird auf der obersten Ebene, neben outer(), definiert und nicht darin.
GLOBAL
├── name = "Global"
├── outer()
└── inner()
Daher führt der Suchpfad direkt zum globalen Umfeld:
inner
↓
Global
Er geht nicht über outer hinweg:
inner
↓
outer ❌
Dass outer() inner() aufgerufen hat, ändert nichts. Der Aufrufort bestimmt den Aufrufstapel; der Definitionsort bestimmt den lexikalischen Gültigkeitsbereich.
Lexikalische Umgebungen: Die Mechanismen hinter dem Gültigkeitsbereich
Die ECMAScript-Spezifikation modelliert den Gültigkeitsbereich mit lexikalischen Umgebungen. Jede davon speichert zwei Dinge:
- die in dieser Umgebung gebundenen Identifikatoren
Als mentales Modell:
Lexical Environment
│
├── Environment Record
└── Outer Environment Reference
Neue Spezifikationsversionen fügen den äußeren Link als Feld [[OuterEnv]] in das Environment Record ein; das Modell bleibt unverändert.
Das Environment Record enthält die Bindungen
Nehmen wir eine Funktion mit zwei lokalen Konstanten:
function dashboard() {
const user = "Jashan";
const role = "Admin";
}
Konzeptionell verknüpft ihr Environment Record Namen mit Werten:
dashboard Environment
user → "Jashan"
role → "Admin"
Das ist ein Modell, kein Objekt, das Ihr Code inspizieren kann.
Die äußere Referenz verbindet Umgebungen miteinander
Jede Umgebung weist auf diejenige hin, die sie lexikalisch umgibt:
const appName = "HealthApp";
function dashboard() {
const user = "Jashan";
}
Für dashboard() sieht das so aus:
dashboard Environment
│
├── user = "Jashan"
└── Outer
↓
Global Environment
Dieser Zeiger macht eine Abfrage außerhalb des lokalen scopes möglich. Er wird bei der Erstellung der Funktion basierend darauf gesetzt, wo sie definiert wurde, weshalb der Aufrufort nicht von Bedeutung ist.
Aufspüren der vollständigen Kette
Zurück zum ursprünglichen Beispiel mit den drei zusammengeführten Protokollen:
const appName = "HealthApp";
function dashboard() {
const user = "Jashan";
function showProfile() {
const status = "Active";
console.log(appName, user, status);
}
showProfile();
}
dashboard();
Wenn showProfile() ausgeführt wird, sind ihre Umgebungen wie folgt miteinander verbunden:
SHOWPROFILE ENVIRONMENT
│
├── status = "Active"
└── Outer
↓
DASHBOARD ENVIRONMENT
│
├── user = "Jashan"
└── Outer
↓
GLOBAL ENVIRONMENT
│
├── appName = "HealthApp"
└── Outer
↓
null
Die äußere Referenz der globalen Umgebung ist null, wodurch die Kette endet.
Scope-Kette und Identifikatorauflösung
Die Scope-Kette ist der Weg, den der Interpreter nimmt, um einen Identifikator zu finden: Zuerst wird die aktuelle Umgebung überprüft, anschließend die äußere Referenz, bis der Name gefunden wird oder die Kette endet.
Current Environment
↓
Outer Environment
↓
Outer Environment
↓
Global
↓
null
Das Finden der Bindung entlang dieses Weges wird als Identifikatorauflösung bezeichnet. Nehmen wir die drei Abfragen:
console.log(status);
console.log(user);
console.log(appName);
Lösen von status
Wird direkt in der lokalen Umgebung gefunden:
showProfile
↓
status found ✅
Lösen von user
Fehlt lokal, wurde ein Level höher gefunden:
showProfile ❌
↓
dashboard
↓
user found ✅
Lösen von appName
Fehlt in beiden Funktionen, wurde in der globalen Umgebung gefunden:
showProfile ❌
↓
dashboard ❌
↓
Global
↓
appName found ✅
In jedem Fall stoppt die Suche bei dem ersten Treffer. Für einen genaueren Blick auf den Suchalgorithmus selbst siehe wie JavaScripts Scope-Chain Variablen tatsächlich auflöst.
Wenn die Auflösung fehlschlägt
Betrachten Sie eine Funktion, die einen Namen liest, der von niemandem deklariert wurde:
function showProfile() {
console.log(age);
}
und einen Aufruf davon:
showProfile();
Falls age in keiner der erreichbaren Umgebungen vorhanden ist, endet die Suche:
showProfile
↓
Global
↓
null
Der Engine ist es nicht möglich, den Namen aufzulösen, wodurch ein Fehler ausgelöst wird:
ReferenceError: age is not defined
Das unterscheidet sich von einer Variablen, die den Wert undefined enthält, oder von einer let-Bindung, die noch nicht initialisiert wurde (die temporäre Todeszone). Diese drei Zustände lassen sich leicht verwechseln:
undefined
uninitialized
undeclared
Drei Regeln, die die meisten Fälle abdecken
Die Suche erfolgt nur nach außen
Inner
↓
Outer
↓
Global
Innerer Code kann externe Variablen lesen. Externer Code kann nicht in einen verschachtelten Scope vordringen, um dort deklarierte Variablen zu lesen.
Die nächstgelegene Bindung gewinnt
Wenn derselbe Name auf mehreren Ebenen existiert, wird die nächstgelegene verwendet:
const user = "Global User";
function dashboard() {
const user = "Dashboard User";
function showProfile() {
console.log(user);
}
showProfile();
}
Ausgabe:
Dashboard User
Die Suche endet in dashboard, also einen Schritt höher:
showProfile ❌
↓
dashboard ✅
↓
STOP
Die globale Variable user wird niemals konsultiert. Eine innere Deklaration, die eine äußere verdeckt, wird als Verdeckung bezeichnet.
Nichts wird in verschachtelte Funktionen kopiert
Ein verlockendes, aber falsches Bild ist, dass innere Funktionen Kopien der äußeren Variablen erhalten:
showProfile
├── copied user
└── copied appName
Das genaue Bild ist eine Kette von Referenzen:
showProfile
↓
dashboard
↓
Global
Durch das Abfragen über einen Link sieht die innere Funktion stets den aktuellen Wert der äußeren Bindung – das ist es, was Closures funktionieren lässt.
Bereichskette gegen Aufrufstapel
Hier ist das Beispiel mit der Falle erneut:
const name = "Global";
function outer() {
const name = "Outer";
inner();
}
function inner() {
console.log(name);
}
outer();
Während inner() ausgeführt wird, sieht der Aufrufstapel so aus:
┌──────────────────┐
│ inner FEC │ ← Current
├──────────────────┤
│ outer FEC │
├──────────────────┤
│ Global EC │
└──────────────────┘
outer() befindet sich eindeutig auf dem Stapel, da es inner() aufgerufen hat. Dennoch gibt inner() weiterhin Ausgabe:
Global
denn die Abfrage folgt der Bereichskette, und inner’s Kette führt direkt zum globalen Umfeld. Die beiden Strukturen beantworten unterschiedliche Fragen:
- Der Aufrufstapel verfolgt die derzeit aktiven Ausführungskontexte; die Gültigkeitskette löst Identifikatoren auf.
- Der Aufrufstapel ist nach der Reihenfolge geordnet, in der eine Funktion von einer anderen aufgerufen wird; die Gültigkeitskette wird durch die Nestung des Quellcodes bestimmt.
- Der Aufrufstapel folgt dem Prinzip „Letzter hineingetan, erster herausgeholt“; die Gültigkeitskette folgt den Verknüpfungen zum äußeren Umfeld.
- Der Aufrufstapel beantwortet die Frage „Was läuft gerade?“, die Gültigkeitskette beantwortet die Frage „Wo existiert dieser Name?“
Ausführungskontexte und lexikalische Umgebungen
Ausführungskontext
Ein Ausführungskontext enthält den Zustand, der während des Laufs von Code benötigt wird. Es gibt zwei Hauptarten:
Global Execution Context
Function Execution Context
Lexikalische Umgebung
Eine lexikalische Umgebung enthält Bindungen von Identifikatoren sowie die Verknüpfung zu ihrem äußeren Umfeld. Ein Ausführungskontext verweist auf seine Umgebung, ungefähr so:
Execution Context
│
└── environment information
↓
Lexical Environment
│
├── bindings
└── outer environment
Daher sind die beiden nicht dasselbe:
Execution Context ≠ Lexical Environment
Der Ausführungskontext erklärt, wie Code ausgeführt wird; die lexikalische Umgebung erklärt, wie Namen gefunden werden.
Lexikaler Gültigkeitsbereich in React-Komponenten
React-Code verlässt sich ständig auf den lexikalen Gültigkeitsbereich. Betrachten Sie einen Ereignishandler, der innerhalb einer Komponente definiert ist:
function UserDashboard({ userId }: { userId: string }) {
const role = "Admin";
function handleProfileClick() {
console.log(userId);
console.log(role);
}
return (
<button onClick={handleProfileClick}>
View Profile
</button>
);
}
handleProfileClick() deklariert keinen dieser Namen:
userId
role
Doch es verwendet sie dennoch, weil er innerhalb von UserDashboard geschrieben wurde:
handleProfileClick
↓
UserDashboard
├── userId
└── role
Derselbe Mechanismus gilt für Props, State, Callbacks, Timer, Promises und Hooks. Jede Renderung erstellt eine neue Umgebung mit den Werten dieser Renderung, und die dabei definierten Handler beziehen sich genau auf diese Werte. Das ist die Grundlage von Closures sowie von veralteten Closures, bei denen ein Callback aus einer früheren Renderung weiterhin alten State liest.
Häufige Missverständnisse
„Die Abfrage folgt dem Aufrufer“
Das ist nicht der Fall. Entscheidend ist die Definitionsebene:
Where function is called ❌
Where function is defined ✅
„Der Aufrufstapel und die Scope-Kette sind dasselbe“
Sie dienen unterschiedlichen Zwecken:
Call Stack → execution
Scope Chain → identifier lookup
„Außenliegender Code kann innere Variablen lesen“
Die Abfrage verläuft einseitig:
Inner → Outer → Global ✅
Outer → Inner ❌
„var berücksichtigt Blöcke“
Das tut es nicht; nur let und const tun das:
var → function-scoped
let/const → block-scoped
„Eine lexikalische Umgebung ist ein gewöhnliches Objekt“
Es handelt sich um ein Konzept aus den Spezifikationen; die Interpreter implementieren es frei, und Ihr Code kann darauf keinen Zugriff erhalten.
Ein mentales Modell zum Behalten
Das Thema lässt sich auf eine Kette von Fragen zusammenfassen:
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
Wichtige Erkenntnisse
- Der Gültigkeitsbereich bestimmt, wo ein Name verwendet werden kann; JavaScript verfügt über globale, funktionale, blockbasierte und modulare Gültigkeitsbereiche, wobei
varauf Funktionen undlet/constauf Blöcke beschränkt sind. - Der Gültigkeitsbereich ist lexikalisch: Die äußere Umgebung einer Funktion ist dort festgelegt, wo sie definiert wird, sodass ein Aufruf von einem anderen Ort niemals das ändert, was sie sehen kann.
- Die Auflösung von Identifikatoren erfolgt vom aktuellen Umfeld aus nach außen, stoppt bei der ersten Übereinstimmung (Verdeckung) und wirft einen
ReferenceErroraus, wenn sie das Ende erreicht. - Innere Funktionen halten Referenzen auf die äußeren Umgebungen, nicht Kopien – das ist die Grundlage für Schließfunktionen sowie für veraltete Schließfunktionen in React.