12 Lexikalisches Scoping

Das Konzept des Scoping ist grundlegend für jede Programmiersprache und bestimmt, wo und wie auf Variablen zugegriffen werden kann. JavaScript verwendet lexikalisches Scoping (auch als statisches Scoping bezeichnet), was bedeutet, dass die Sichtbarkeit und Lebensdauer von Variablen durch die statische Struktur des Quellcodes bestimmt wird.

12.1 Grundlagen des Scoping

Der “Scope” (Geltungsbereich) einer Variablen definiert, in welchen Bereichen des Codes diese Variable sichtbar und verwendbar ist. In JavaScript gibt es mehrere Arten von Scopes:

  1. Globaler Scope: Variablen, die außerhalb von Funktionen oder Blöcken deklariert werden
  2. Funktions-Scope: Variablen, die innerhalb einer Funktion deklariert werden
  3. Block-Scope: Variablen, die innerhalb eines Blocks (durch geschweifte Klammern begrenzt) mit let oder const deklariert werden

12.2 Globaler Scope

Variablen im globalen Scope sind überall im Code zugänglich:

// Globale Variable
const appName = "MeineApp";

function displayAppName() {
  // Zugriff auf globale Variable innerhalb einer Funktion
  console.log(appName);
}

displayAppName(); // "MeineApp"

Globale Variablen sollten sparsam eingesetzt werden, da sie zu Namenskonflikten und schwer nachvollziehbarem Code führen können. In Browsern werden globale Variablen als Eigenschaften des globalen window-Objekts gespeichert.

12.3 Function-Scope vs. Block-Scope

Die Art der Variablendeklaration bestimmt den Typ des Scopes:

12.3.1 Function-Scope mit var

Variablen, die mit var deklariert werden, haben Funktions-Scope. Das bedeutet, sie sind innerhalb der gesamten Funktion sichtbar, auch wenn sie in einem verschachtelten Block definiert wurden:

function beispielFunktion() {
  var x = 1;
  
  if (true) {
    var y = 2; // y hat Funktions-Scope, nicht Block-Scope
    console.log(x); // 1 - x ist hier sichtbar
  }
  
  console.log(y); // 2 - y ist auch außerhalb des if-Blocks sichtbar
}

beispielFunktion();
// console.log(x); // ReferenceError: x ist außerhalb der Funktion nicht sichtbar

Diese Eigenschaft von var kann zu unerwarteten Ergebnissen führen, besonders in Schleifen:

function zählen() {
  for (var i = 0; i < 3; i++) {
    // i existiert im Funktions-Scope
  }
  
  console.log(i); // 3 - i ist auch nach der Schleife sichtbar
}

12.3.2 Block-Scope mit let und const

Mit ES6 wurden let und const eingeführt, die Block-Scope haben. Variablen, die mit diesen Keywords deklariert werden, sind nur innerhalb des Blocks sichtbar, in dem sie definiert wurden:

function blockScopeBeispiel() {
  let x = 1;
  
  if (true) {
    let y = 2; // y hat Block-Scope
    const z = 3; // z hat ebenfalls Block-Scope
    console.log(x); // 1 - x ist hier sichtbar (aus äußerem Scope)
  }
  
  // console.log(y); // ReferenceError: y ist außerhalb des if-Blocks nicht sichtbar
  // console.log(z); // ReferenceError: z ist außerhalb des if-Blocks nicht sichtbar
}

Dies ist besonders nützlich in Schleifen, wo jede Iteration ihren eigenen Scope erhält:

function zählenMitLet() {
  for (let i = 0; i < 3; i++) {
    // Jede Iteration hat ihren eigenen i-Wert
  }
  
  // console.log(i); // ReferenceError: i ist außerhalb der Schleife nicht sichtbar
}

Der Block-Scope von let und const macht den Code robuster und vorhersehbarer. Daher wird in modernem JavaScript die Verwendung von let und const gegenüber var empfohlen.

12.4 Scope-Hierarchie und Verschachtelung

Scopes in JavaScript bilden eine Hierarchie. Innere Scopes haben Zugriff auf Variablen aus äußeren Scopes, aber nicht umgekehrt. Dies wird oft als “Scope-Kette” bezeichnet:

const global = "Ich bin global";

function äußereFunktion() {
  const äußer = "Ich bin in der äußeren Funktion";
  
  function innereFunktion() {
    const inner = "Ich bin in der inneren Funktion";
    
    console.log(global);  // "Ich bin global" - Zugriff auf globale Variable
    console.log(äußer);   // "Ich bin in der äußeren Funktion" - Zugriff auf Variable aus äußerem Scope
    console.log(inner);   // "Ich bin in der inneren Funktion" - lokale Variable
  }
  
  innereFunktion();
  
  console.log(global);  // "Ich bin global" - Zugriff auf globale Variable
  console.log(äußer);   // "Ich bin in der äußeren Funktion" - lokale Variable
  // console.log(inner); // ReferenceError: inner ist nicht zugänglich im äußeren Scope
}

äußereFunktion();

12.5 Variablenschatten (Variable Shadowing)

Wenn eine Variable im inneren Scope denselben Namen hat wie eine Variable im äußeren Scope, “verschattet” sie die äußere Variable:

const wert = "global";

function test() {
  const wert = "lokal";
  console.log(wert); // "lokal" - innere Variable verschattet die äußere
}

test();
console.log(wert); // "global" - äußere Variable bleibt unverändert

Dies ist kein Fehler, kann aber zu Verwirrung führen. Es ist oft besser, eindeutige Variablennamen zu verwenden.

12.6 Lexikalisches this

Anders als Variablen wird das this-Keyword in JavaScript nicht lexikalisch gebunden (außer in Pfeilfunktionen). Der Wert von this hängt davon ab, wie eine Funktion aufgerufen wird:

const objekt = {
  name: "Beispielobjekt",
  methode: function() {
    console.log(this.name); // "Beispielobjekt" - this bezieht sich auf objekt
    
    function innereFunktion() {
      console.log(this.name); // undefined - this ist hier nicht objekt
    }
    
    innereFunktion();
  }
};

objekt.methode();

Dieses Problem kann mit Pfeilfunktionen gelöst werden, die this lexikalisch binden:

const objekt = {
  name: "Beispielobjekt",
  methode: function() {
    console.log(this.name); // "Beispielobjekt"
    
    // Pfeilfunktion übernimmt this aus dem umgebenden lexikalischen Kontext
    const innereFunktion = () => {
      console.log(this.name); // "Beispielobjekt" - this bezieht sich auf das gleiche Objekt wie in methode
    };
    
    innereFunktion();
  }
};

objekt.methode();

12.7 Variable Hoisting

“Hoisting” beschreibt das Verhalten, bei dem Deklarationen (nicht Initialisierungen) von Variablen und Funktionen an den Anfang ihres Scopes gehoben werden. Dies beeinflusst, wie und wann auf Variablen zugegriffen werden kann.

12.7.1 Hoisting mit var

Bei var-Deklarationen wird die Variable an den Anfang ihres Funktions-Scopes gehoben, aber nicht initialisiert:

function beispiel() {
  console.log(x); // undefined (nicht ReferenceError!)
  var x = 5;
  console.log(x); // 5
}

beispiel();

Dieses Verhalten ist äquivalent zu:

function beispiel() {
  var x; // Deklaration wird gehoisted
  console.log(x); // undefined
  x = 5; // Initialisierung bleibt an ursprünglicher Position
  console.log(x); // 5
}

12.7.2 Hoisting mit let und const

Technisch gesehen werden auch let und const Deklarationen gehoisted, aber im Gegensatz zu var werden sie nicht mit undefined initialisiert. Stattdessen verbleiben sie in der sogenannten “Temporal Dead Zone” (TDZ) bis zur tatsächlichen Deklaration im Code:

function beispiel() {
  // Temporal Dead Zone für x beginnt hier
  console.log(x); // ReferenceError: Cannot access 'x' before initialization
  let x = 5;
  console.log(x); // 5
}

beispiel();

Die TDZ macht das Verhalten von let und const intuitiver und hilft, Fehler frühzeitig zu erkennen.

12.7.3 Funktions-Hoisting

Im Gegensatz zu Variablen werden Funktionsdeklarationen vollständig gehoisted - nicht nur die Deklaration, sondern auch die Implementierung:

// Funktionsaufruf vor Definition funktioniert
console.log(addieren(2, 3)); // 5

// Funktionsdeklaration wird vollständig gehoisted
function addieren(a, b) {
  return a + b;
}

Funktionsausdrücke werden jedoch wie Variablen behandelt und unterliegen den gleichen Hoisting-Regeln:

// console.log(add(2, 3)); // TypeError: add is not a function

var add = function(a, b) {
  return a + b;
};

// Mit let oder const
// console.log(multiply(2, 3)); // ReferenceError: Cannot access 'multiply' before initialization

const multiply = function(a, b) {
  return a * b;
};

12.8 Praktische Auswirkungen des lexikalischen Scoping

Das Verständnis des lexikalischen Scopings ist entscheidend für:

12.8.1 Closures

Lexikalisches Scoping ermöglicht Closures, eine der mächtigsten Eigenschaften von JavaScript:

function äußereFunktion() {
  const zähler = 0; // Variable im äußeren Scope
  
  function erhöhen() {
    return ++zähler; // Zugriff auf Variable aus äußerem Scope
  }
  
  return erhöhen; // Rückgabe der inneren Funktion
}

const zählerFunktion = äußereFunktion();
console.log(zählerFunktion()); // 1
console.log(zählerFunktion()); // 2

Die zurückgegebene Funktion behält Zugriff auf die Variablen ihres lexikalischen Umfelds, auch nachdem die äußere Funktion bereits abgeschlossen ist.

12.8.2 Module und Kapselung

Mit lexikalischem Scoping können private Variablen und Funktionen implementiert werden:

// Modul-Muster mit IIFE
const counter = (function() {
  // Private Variable im Scope der IIFE
  let count = 0;
  
  // Öffentliche API
  return {
    increment() {
      return ++count;
    },
    decrement() {
      return --count;
    },
    getValue() {
      return count;
    }
  };
})();

counter.increment(); // 1
counter.increment(); // 2
// count ist nicht direkt zugänglich

12.8.3 Vermeidung von Namenskonflikten

Lexikalisches Scoping hilft, Namenskonflikte zu vermeiden, indem es die Sichtbarkeit von Variablen begrenzt:

function komponente1() {
  const config = { name: "Komponente 1" };
  // config ist nur in dieser Funktion sichtbar
}

function komponente2() {
  const config = { name: "Komponente 2" };
  // Ein anderes config, kein Konflikt mit komponente1
}

12.9 Beste Praktiken

Für eine effektive Nutzung des lexikalischen Scopings in JavaScript sollten folgende Praktiken beachtet werden:

  1. Verwenden Sie let und const statt var

    // Vermeiden Sie var
    var x = 1;
    
    // Bevorzugen Sie const für Werte, die sich nicht ändern
    const PI = 3.14159;
    
    // Verwenden Sie let für Variablen, die sich ändern können
    let counter = 0;
  2. Minimieren Sie den Geltungsbereich von Variablen

    // Schlecht: Variable mit zu großem Scope
    let i = 0;
    // ... viele Zeilen Code ...
    for (; i < 10; i++) { /* ... */ }
    
    // Besser: Variable mit minimiertem Scope
    for (let i = 0; i < 10; i++) { /* ... */ }
  3. Vermeiden Sie Variablenschatten

    const name = "global";
    
    // Vermeiden Sie die Wiederverwendung von Variablennamen
    function process() {
      // Verwenden Sie einen anderen Namen
      const localName = "local";
      // statt: const name = "local";
    }
  4. Bevorzugen Sie Blockscopes für temporäre Variablen

    // Temporäre Berechnung in eigenem Block
    {
      const temp = getData();
      // Verarbeite temp...
    }
    // temp ist hier nicht mehr zugänglich
  5. Seien Sie vorsichtig mit Hoisting

    // Deklarieren Sie Variablen am Anfang ihres Scopes
    function beispiel() {
      let x; // Alle Deklarationen am Anfang
      let y;
    
      // Dann Code...
      x = 5;
      y = x * 2;
    }