Hoisting ist das Verhalten der JavaScript-Engine, das Deklarationen vor der eigentlichen Ausführung des Codes „nach oben zieht" — nicht physisch im Quelltext, sondern im Scope-Lookup, der vor jeder Execution Phase passiert. Der Begriff selbst kommt nicht aus der Spezifikation, sondern aus der Lehre; was die Spezifikation tatsächlich beschreibt, sind getrennte Phasen für Creation und Execution. Dieser Artikel zeigt, wie sich die fünf relevanten Deklarations-Formen — var, let, const, function, class — beim Hoisting unterscheiden, was die Temporal Dead Zone wirklich ist und welche Anti-Patterns sich nur durch Hoisting-Wissen verstehen lassen.
Was ist Hoisting?
Wenn die Engine eine Funktion oder ein Modul lädt, durchläuft sie zwei logische Phasen: in der Creation Phase scannt sie den gesamten Code und legt für jede Deklaration im Scope einen passenden Eintrag an — Funktions-Body, Variablen-Slot, Class-Binding. Erst danach beginnt die Execution Phase, in der die Anweisungen Zeile für Zeile abgearbeitet werden. Der Effekt aus Sicht des Programmierers: Namen, die weiter unten deklariert sind, sind schon vorher bekannt. Genau dieses Phänomen heißt Hoisting.
Der genaue Charakter dieses „Bekanntseins" hängt von der Deklarations-Form ab. Die MDN unterscheidet vier Hoisting-Verhalten — von „voll nutzbar" (Funktionen) bis „existent, aber Zugriff wirft Fehler" (let, const, class).
gruss(); // funktioniert — function-Hoisting
function gruss() {
console.log('Hallo!');
}
console.log(zaehler); // undefined — var-Hoisting (mit undefined)
var zaehler = 5;
// console.log(name); // ReferenceError — TDZ
let name = 'Anna';Hallo!
undefinedFunction-Hoisting — vollständig
Eine Function-Declaration (function name() { ... }) wird bei der Creation Phase vollständig gehoisted: Name und Body stehen ab dem ersten Moment im Scope zur Verfügung. Das ist Type-1-Hoisting nach MDN-Klassifikation und das einzige Hoisting, das aus Sicht des Aufrufers tatsächlich „magisch" wirkt.
// Aufruf VOR der Definition
console.log(addiere(2, 3)); // 5
function addiere(a, b) {
return a + b;
}
// Auch verschachtelte Aufrufe und Rekursion funktionieren
console.log(fakultaet(5)); // 120
function fakultaet(n) {
return n <= 1 ? 1 : n * fakultaet(n - 1);
}5
120Praktisch nutzen viele Codebasen diese Eigenschaft, indem sie die Funktions-Definitionen unten und den Top-Level-Aufruf oben platzieren — ein Stil, den manche bevorzugen, andere verpönen.
var-Hoisting — Slot mit undefined
var-Deklarationen werden in der Creation Phase mit dem Wert undefined initialisiert. Das hat eine wichtige Konsequenz: Lesender Zugriff vor der Zuweisungs-Zeile wirft keinen Fehler, sondern liefert undefined. Ein Verhalten, das mehr Bugs versteckt als es löst.
console.log(typeof x); // 'undefined'
console.log(x); // undefined — KEIN Fehler
var x = 5;
console.log(x); // 5
// Mental wird das vom Compiler so umgeschrieben:
// var x; // Creation Phase: Slot mit undefined
// console.log(x); // Execution Phase: liest aus Slot
// x = 5; // Zuweisungundefined
undefined
5Die berüchtigte Interview-Frage lautet: „Was gibt console.log(x); var x = 5; aus?" — Antwort: undefined, eben wegen des Hoistings.
let/const-Hoisting + Temporal Dead Zone
let und const werden ebenfalls gehoisted — die Engine kennt ihren Namen ab Block-Beginn. Der Unterschied zu var: sie sind in dieser Phase uninitialized, nicht undefined. Jeder Zugriff in der Phase zwischen Block-Start und Deklaration wirft ReferenceError. Dieser Bereich heißt Temporal Dead Zone.
{
// Block-Start — TDZ beginnt hier
// console.log(x); // ReferenceError: Cannot access 'x' before initialization
// typeof x; // ReferenceError — auch typeof greift in TDZ
let x = 1; // TDZ endet — x ist initialisiert
console.log(x); // 1
}Wichtig: die TDZ beginnt am Block-Anfang, nicht erst eine Zeile vor der Deklaration. Eine Variable mit gleichem Namen aus einem äußeren Scope wird im inneren Block durch das let/const „verdunkelt" — sogar in den Zeilen davor.
const x = 'aussen';
{
// console.log(x); // ReferenceError — innerer x "taintet" den ganzen Block,
// obwohl die Deklaration erst unten kommt
const x = 'innen';
console.log(x); // 'innen'
}Function-Expression vs. Function-Declaration
Eine Function-Expression — also eine Funktion, die einer Variable per const, let oder var zugewiesen wird — folgt den Hoisting-Regeln der jeweiligen Variable, nicht denen einer normalen Function-Declaration. Das ist eine der häufigsten Verwechslungen bei Anfängern.
// Function-Declaration — voll gehoisted
sagHallo(); // 'Hallo!'
function sagHallo() { console.log('Hallo!'); }
// Function-Expression mit const — TDZ wie const
// sagWelt(); // ReferenceError: Cannot access 'sagWelt' before initialization
const sagWelt = function() { console.log('Welt!'); };
sagWelt(); // 'Welt!'
// Arrow-Function mit const — gleiches Verhalten
// sagBye(); // ReferenceError
const sagBye = () => console.log('Bye!');
sagBye(); // 'Bye!'Hallo!
Welt!
Bye!Die Konsequenz: wer Funktionen mit const f = () => ... definiert (modern empfohlene Form), kann sie nicht „vor ihrer Zeile" aufrufen. Das ist meist gewollt — Code wird linear lesbar.
Class-Hoisting — TDZ wie let
Klassen werden ebenfalls gehoisted, fallen aber in dieselbe Kategorie wie let und const: existent ab Block-Beginn, aber in TDZ. Ein Aufruf vor der Class-Zeile wirft ReferenceError.
// const a = new Auto(); // ReferenceError: Cannot access 'Auto' before initialization
class Auto {
constructor(marke) { this.marke = marke; }
}
const b = new Auto('VW'); // ok
console.log(b.marke); // 'VW'Das gilt auch für die Erweiterung anderer Klassen: class Sub extends Super wirft, wenn Super zur Zeile noch in TDZ ist.
Praxis: Engine-Phasen — Creation und Execution
Das mentale Modell, das hinter Hoisting steht, lässt sich gut als zwei Pseudo-Phasen einer Funktion lesen. Nehmen wir folgenden Code:
function demo() {
console.log(a, b, c);
var a = 1;
let b = 2;
const c = 3;
inner();
function inner() { console.log('inner'); }
}So liest die Engine das beim Aufruf von demo() — zuerst die Creation Phase, dann die Execution Phase:
// === Creation Phase ===
// - var a : Slot anlegen, mit undefined initialisieren
// - let b : Slot anlegen, status: uninitialized (TDZ)
// - const c : Slot anlegen, status: uninitialized (TDZ)
// - function inner : Slot anlegen UND Body zuweisen
// === Execution Phase ===
// console.log(a, b, c)
// -> a = undefined ok
// -> b ReferenceError — Funktion bricht hier ab
//
// (Der Rest wird gar nicht erreicht.)Diese Trennung ist der Schlüssel zum Verstehen aller Hoisting-Effekte. Was in der Creation Phase passiert, bestimmt, was in der Execution Phase erlaubt ist.
Hoisting-Reihenfolge bei Konflikten
Wenn im selben Scope eine Function-Declaration und ein var mit dem gleichen Namen stehen, gewinnt am Ende der Creation Phase die Function — der Slot zeigt auf den Funktions-Body. Während der Execution Phase kann eine var-Zuweisung dann den Slot wieder mit einem anderen Wert überschreiben.
console.log(typeof foo); // 'function' — function gewinnt das Hoisting
var foo = 'eine String';
function foo() { return 42; }
console.log(typeof foo); // 'string' — var-Zuweisung hat überschriebenfunction
stringMit let oder const an dieser Stelle wäre der Code ein SyntaxError beim Parsen — Re-Deklaration verboten. Genau deshalb sind diese modernen Formen einfacher zu durchschauen.
Anti-Patterns durch Hoisting
Hoisting ist die Wurzel mehrerer klassischer Bug-Muster. Drei davon sind besonders verbreitet:
varin einemif-Block, der außerhalb des Blocks weiterlebt — ein Klassiker beim Refactoring.- Function-Declarations in Blöcken, deren Verhalten zwischen Strict und Sloppy Mode unterschiedlich ist.
- Variable, die vor der Zeile verwendet wird, an der sie deklariert wird — bei
varein versteckterundefined, beiletein klarerReferenceError.
// Anti-Pattern 1: var lebt außerhalb des if-Blocks
function f1() {
if (true) { var x = 1; }
console.log(x); // 1 — überraschend
}
// Anti-Pattern 2: function-Declaration in if-Block
// (in strict mode block-scoped, in sloppy mode hochgehoben)
function f2(flag) {
if (flag) {
function helper() { return 'ja'; }
}
// helper() — Verhalten je nach Modus inkonsistent
}
// Anti-Pattern 3: var-Hoisting verwischt Lesbarkeit
function f3() {
console.log(name); // undefined — wirkt wie ein Bug
var name = 'Anna';
}In modernem Code, der konsequent let/const und ESM nutzt, verschwindet das meiste davon — die Engine zwingt zur sauberen Reihenfolge.
Best Practices
Drei Regeln decken praktisch alle Fälle gut ab:
- Deklarationen am Anfang des Scopes platzieren. Auch wenn Hoisting es technisch erlaubt, weiter unten zu deklarieren — Lesbarkeit gewinnt, wenn die Reihenfolge der physischen Zeilen mit der logischen Reihenfolge übereinstimmt.
function-Declarations nicht in Blöcken nutzen. Stattdessen eine Variable mit Function-Expression:const helper = () => .... Das vermeidet die Mode-Unterschiede zwischen Strict und Sloppy.let/conststattvar. Die TDZ macht Hoisting-Bugs zu lauten Fehlern, statt sie alsundefined-Werte zu verstecken.
| Deklaration | Hoisted? | Initialisiert mit | Zugriff vor Zeile |
|---|---|---|---|
function name(){} | ja | Body | erlaubt |
var x | ja | undefined | liefert undefined |
let x | ja | uninitialized | ReferenceError (TDZ) |
const x | ja | uninitialized | ReferenceError (TDZ) |
class C {} | ja | uninitialized | ReferenceError (TDZ) |
import (statisch) | ja | gebunden zur Linkzeit | erlaubt (nach Linking) |
Typische Fallen
typeof undeclared ist 'undefined' — typeof in TDZ wirft
typeof unbekannt liefert 'undefined' ohne Fehler — eine alte Konvention für Feature-Detection (typeof window etc.). Aber: typeof x innerhalb der TDZ einer let/const-Variablen wirft ReferenceError. Das überrascht regelmäßig: ein Identifier, der existiert, fühlt sich „weniger sicher" an als einer, der nicht existiert.
function-Declarations in if-Blöcken sind in strict verboten
Im Sloppy Mode werden sie inkonsistent zwischen Engines behandelt — manche hoisten in den Function-Scope, andere block-scopen sie. Strict mode definiert sie streng als block-scoped. Empfehlung: in Blöcken keine function name() {...} verwenden, stattdessen const name = () => ....
TDZ besteht ab Block-Anfang, NICHT ab let-Statement
Die TDZ einer let/const-Variable beginnt mit der schließenden geschweiften Klammer des umgebenden Blocks — also vom Block-Anfang an, nicht erst eine Zeile davor. Effekt: ein äußeres x wird im inneren Block schon „verdunkelt", obwohl die innere Deklaration erst weiter unten steht. Zugriff dazwischen wirft ReferenceError.
var-Hoisting greift auch in for-Schleifen-Initializern
for (var i = 0; i < 5; i++) { ... } macht i nach der Schleife im Function-Scope sichtbar, mit Wert 5. Mit let ist i auf den for-Block beschränkt und nach der Schleife nicht zugänglich. Klassischer Bug-Vektor in alten Codebasen.
Klassische Interview-Frage: var vs. console.log davor
console.log(x); var x = 5; gibt undefined aus — nicht ReferenceError, weil var bereits in der Creation Phase mit undefined initialisiert wurde. Mit let wäre das ein Fehler. Kandidaten, die diese Differenz nicht kennen, fallen in fast jedem JS-Interview hier durch.
function-Declarations werden compile-time analysiert
Daher kann eine Funktion sich selbst rekursiv aufrufen, ohne dass je eine TDZ entsteht. Auch wechselseitige Rekursion zwischen mehreren function f() {}-Definitionen funktioniert, egal in welcher Reihenfolge sie geschrieben sind. Bei const f = () => ...-Definitionen geht das nur, wenn die Aufrufe innerhalb der Funktions-Bodies passieren — nicht in der Initialisierung.
import-Statements werden gehoisted und bei Linking aufgelöst
Statische import-Anweisungen sind „voll gehoisted" — sie werden zur Linking-Phase aufgelöst, bevor irgendein Code des Moduls läuft. Daher kann der Quelltext ein import auch erst weiter unten platzieren; üblich ist aber das Konvention, sie alle oben zu sammeln. Dynamisches import() ist davon nicht betroffen — das ist ein gewöhnlicher Funktions-Aufruf.
let f = function() {} hat TDZ — der Hoisting-Trick funktioniert nicht
let f = function() {} oder const f = () => ... haben TDZ wie jede let/const-Variable. Der klassische Trick „Funktion vor Definition aufrufen" funktioniert NUR mit function name() {}-Declarations. In modernem Code ist das meist kein Problem, weil Code-Reihenfolge ohnehin top-down geschrieben wird.