Startseite / Artikel

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.

Verlag: REACTAPP.TOP11 Min. Lesezeit2586 Wörter

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:

  1. die in dieser Umgebung gebundenen Identifikatoren

  • Ein Link zur Umgebung, die sie umgibt
  • 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 var auf Funktionen und let/const auf 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 ReferenceError aus, 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.