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.