JavaScript kennt drei Schlüsselwörter, mit denen sich eine Variable deklarieren lässt: das historische var aus ES1 (1997), sowie let und const, die mit ES2015 nachgereicht wurden. Die drei verhalten sich in Scope, Hoisting, Re-Assignment und Re-Deklaration so unterschiedlich, dass die Wahl der Deklaration einen sichtbaren Einfluss auf Lesbarkeit und Robustheit des Codes hat. Dieser Artikel zeigt im Detail, wo die Unterschiede liegen, warum const in modernem Code die Standard-Wahl ist und warum var außerhalb von Legacy-Bestand kaum noch eine Rolle spielt.

Drei Wege, eine Variable zu deklarieren

In den ersten Versionen der Sprache gab es ausschließlich var. Die Probleme dieses Schlüsselworts — Function-Scope statt Block-Scope, das verwirrende Hoisting-Verhalten, das stillschweigende Re-Deklarieren — wurden über Jahre durch Coding-Konventionen kaschiert (IIFE-Pattern, Linter-Regeln). Mit ECMAScript 2015 bekam JavaScript zwei neue Deklarations-Schlüsselwörter, die diese Probleme an der Wurzel adressieren: let für veränderliche Bindings, const für unveränderliche.

JavaScript drei-deklarationen.js
var   a = 1;   // ES1 — function-scoped, hoisted mit undefined
let   b = 2;   // ES2015 — block-scoped, TDZ vor Deklaration
const c = 3;   // ES2015 — block-scoped, TDZ, kein Re-Assignment

a = 11;        // ok
b = 22;        // ok
// c = 33;     // TypeError: Assignment to constant variable
Eigenschaftvarletconst
eingeführt mitES1 (1997)ES2015ES2015
ScopeFunction / GlobalBlockBlock
Hoistingja, init. mit undefinedja, aber TDZja, aber TDZ
Re-Assignmenterlaubterlaubtverboten (TypeError)
Re-Deklarationerlaubt (silently)SyntaxErrorSyntaxError
Top-Level → globalThisja (Browser-Global)neinnein
Initialisierung Pflichtneinneinja

const im Detail — Konstante Bindung, nicht konstanter Wert

const erzeugt eine unveränderliche Bindung zwischen Name und Wert. Was sich nicht mehr ändern lässt, ist die Zuordnung — der Slot, der Name auf Wert abbildet. Was sich sehr wohl ändern lässt, ist der innere Zustand des referenzierten Objekts, falls es eines ist. Dieser Unterschied ist die häufigste Verwirrung rund um const.

JavaScript const-binding-vs-value.js
const arr = [1, 2, 3];
arr.push(4);          // erlaubt — Inhalt mutiert
arr[0] = 99;          // erlaubt — Inhalt mutiert
console.log(arr);     // [99, 2, 3, 4]

// arr = [];          // TypeError — Bindung darf nicht neu gesetzt werden

const obj = { name: 'Anna' };
obj.name = 'Bea';     // erlaubt — Property mutiert
obj.alter = 30;       // erlaubt — neue Property
// obj = {};          // TypeError
Output
[ 99, 2, 3, 4 ]

Wer eine echte Wert-Immutability braucht, kombiniert const mit Object.freeze() (oberflächlich) oder einer rekursiven Deep-Freeze-Variante. const allein bietet diese Garantie nicht.

Eine Eigenheit: const muss bei der Deklaration initialisiert werden. const x; ist ein SyntaxError — anders als bei var und let.

JavaScript const-init-required.js
// const x;          // SyntaxError: Missing initializer in const declaration

const x = undefined; // technisch ok, aber kaum sinnvoll

let im Detail — Block-Scope mit Re-Assignment

let ist die Wahl, wenn eine Variable im Lauf ihres Lebens neue Werte zugewiesen bekommen muss — Schleifen-Zähler, Akkumulatoren, Flags. Sie ist block-scoped, das heißt: ihre Sichtbarkeit endet an der nächsten schließenden geschweiften Klammer.

JavaScript let-block-scope.js
function rabatt(preis) {
    let faktor = 1;
    if (preis > 100) {
        let bonus = 0.1;          // nur in diesem Block sichtbar
        faktor = 1 - bonus;
    }
    // console.log(bonus);        // ReferenceError
    return preis * faktor;
}

console.log(rabatt(80));   // 80
console.log(rabatt(150));  // 135
Output
80
135

Der Block kann jeder geschweifte Block sein: if, for, while, switch-Case (mit Klammern), try/catch und auch ein nackter { ... }-Block. Diese Lokalisierung der Sichtbarkeit ist einer der wichtigsten Hebel, um Bugs durch versehentliches Überschreiben zu vermeiden.

var und seine Probleme

var hat in modernem Code drei strukturelle Schwächen. Erstens ist es function-scoped, nicht block-scoped: ein var in einem if-Block lebt bis zum Ende der umgebenden Funktion. Zweitens wird es mit undefined gehoisted: lesender Zugriff vor der Deklaration liefert undefined statt eines Fehlers — eine klassische Bug-Quelle. Drittens erzeugt var auf Top-Level einer klassischen Skript-Datei eine Property auf dem globalen Objekt (window im Browser, globalThis allgemein).

JavaScript var-probleme.js
function demo() {
    console.log(x); // undefined — wegen Hoisting
    if (true) {
        var x = 5;  // function-scope, sichtbar in der ganzen Funktion
    }
    console.log(x); // 5
}
demo();

// Top-Level (klassisches Skript, kein ESM):
var globalLeak = 'hallo';
console.log(globalThis.globalLeak); // 'hallo'
Output
undefined
5
hallo

In ES Modules (.mjs oder "type": "module") wird der dritte Effekt entschärft: Top-Level-var ist dort modul-lokal, nicht global. Die ersten beiden Schwächen bleiben aber bestehen.

Block-Scope vs. Function-Scope — die Closure-Falle

Das klassische Beispiel, das den Unterschied zwischen var und let praktisch greifbar macht, ist eine for-Schleife mit asynchronem Callback. Mit var teilen sich alle Iterationen den gleichen Variable-Slot — am Ende der Schleife steht dort der finale Wert, und genau diesen Wert sehen die Callbacks.

JavaScript closure-falle.js
// Mit var — der klassische Bug
for (var i = 0; i < 3; i++) {
    setTimeout(() => console.log('var:', i), 0);
}
// var: 3
// var: 3
// var: 3

// Mit let — pro Iteration ein neuer Scope
for (let j = 0; j < 3; j++) {
    setTimeout(() => console.log('let:', j), 0);
}
// let: 0
// let: 1
// let: 2
Output
var: 3
var: 3
var: 3
let: 0
let: 1
let: 2

Der entscheidende Punkt: bei let legt die Engine pro Iteration einen neuen Block-Scope an. Jede Closure schließt damit über ihre eigene Kopie von j. Bei var gibt es nur eine einzige Bindung über die ganze Funktion hinweg.

Temporal Dead Zone (TDZ)

let und const werden technisch ebenfalls gehoisted — das heißt, ihr Name ist im umgebenden Block bereits bekannt, sobald der Block betreten wird. Was unterschieden ist: bis zur Zeile mit der Deklaration sind sie uninitialized. Jeder Zugriff in dieser Phase wirft einen ReferenceError. Der Bereich vom Block-Beginn bis zur Deklaration heißt Temporal Dead Zone.

JavaScript tdz.js
{
    // Block-Beginn — x ist hier schon "existent", aber uninitialized
    // console.log(x);   // ReferenceError: Cannot access 'x' before initialization
    // typeof x;         // ReferenceError — auch typeof greift

    let x = 1;            // ab hier ist x initialisiert und nutzbar
    console.log(x);       // 1
}

Im Gegensatz dazu liefert var in der gleichen Situation undefined und wirft keinen Fehler — was zwar weniger aggressiv ist, aber Bugs verstecken kann.

JavaScript tdz-vs-var.js
function demoVar() {
    console.log(a); // undefined — kein Error
    var a = 1;
}

function demoLet() {
    // console.log(b); // ReferenceError
    let b = 1;
    console.log(b);
}

demoVar();
demoLet();
Output
undefined
1

Die TDZ ist kein Bug, sondern ein bewusstes Design: sie macht aus einer schleichenden Falle (undefined-Werte aus dem Nichts) einen lauten Fehler.

Re-Deklaration im selben Scope

var erlaubt es, denselben Namen im selben Scope mehrfach zu deklarieren — ohne Warnung. Das war historisch praktisch (mehrere Skript-Dateien konnten unabhängig voneinander Variablen einführen, ohne sich gegenseitig zu blockieren), ist aber heute eine Bug-Quelle. let und const werfen in diesem Fall einen SyntaxError beim Parsen.

JavaScript re-deklaration.js
// var — silently erlaubt
var x = 1;
var x = 2;            // ok
console.log(x);       // 2

// let — SyntaxError
// let y = 1;
// let y = 2;         // SyntaxError: Identifier 'y' has already been declared

// Mischen ist ebenfalls verboten:
// var z = 1;
// let z = 2;         // SyntaxError

Der Fehler entsteht zur Parse-Zeit, nicht zur Laufzeit — der ganze Block wird abgelehnt, bevor irgendeine Anweisung ausgeführt wird.

Globale Variablen — var vs. let/const auf Top-Level

In einem klassischen <script>-Block ohne type="module" werden Top-Level-var und function-Deklarationen zu Properties des globalen Objekts. let und const tun das nicht: sie leben in einem separaten, modul-ähnlichen Scope.

JavaScript global-scope.js
// klassisches Skript — KEIN ESM
var   a = 1;
let   b = 2;
const c = 3;

console.log(globalThis.a); // 1   — var landet auf globalThis
console.log(globalThis.b); // undefined
console.log(globalThis.c); // undefined
Output
1
undefined
undefined

In ES Modules ist das Verhalten einheitlich: keine der drei Deklarationen erzeugt eine Property auf globalThis. Wer aus einem Modul heraus eine echte globale Variable braucht, muss sie explizit setzen: globalThis.x = 5;.

Best Practices 2026

Drei Regeln decken praktisch alle Fälle ab:

  1. const ist der Default. Solange die Variable im Lauf ihres Lebens nicht neu zugewiesen wird, ist const die richtige Wahl. Das Lesen wird einfacher, weil ein const dem Leser signalisiert: dieser Name zeigt von hier an immer auf denselben Wert.
  2. let nur, wenn Re-Assignment nötig ist. Schleifen-Variablen, Akkumulatoren in for-Loops, Flags, die ihren Zustand wechseln. Wenn sich beim Refactoring zeigt, dass das let doch nicht reassignt wird, durch const ersetzen.
  3. var praktisch nie. Die einzigen Fälle: Pflege von Legacy-Code, der ohne TypeScript / Linter geschrieben wurde, oder bewusst genutzte Function-Scope-Mechanik (selten und meist besser anders gelöst). In neuem Code ist var ein Geruch.

Praxis: Refactoring von var zu let/const

Eine typische Funktion aus einer Pre-2015-Codebasis und ihre moderne Form:

JavaScript refactor-vorher.js
// Vorher: var-only, function-scoped Variablen, Closure-Falle vermeintlich umgangen
function summen(zahlen) {
    var ergebnisse = [];
    var summe = 0;
    for (var i = 0; i < zahlen.length; i++) {
        summe += zahlen[i];
        ergebnisse.push(summe);
    }
    return ergebnisse;
}
JavaScript refactor-nachher.js
// Nachher: const wo möglich, let für die Akkumulator-Variable
function summen(zahlen) {
    const ergebnisse = [];     // Bindung konstant, Inhalt wandert
    let summe = 0;             // wird re-assigned

    for (let i = 0; i < zahlen.length; i++) {
        summe += zahlen[i];
        ergebnisse.push(summe);
    }
    return ergebnisse;
}

console.log(summen([1, 2, 3, 4])); // [1, 3, 6, 10]
Output
[ 1, 3, 6, 10 ]

Drei Änderungen, drei Gewinne: ergebnisse wird zu const (das Array wird gefüllt, aber nie ersetzt), summe bleibt let (echter Re-Assignment-Fall), i zieht in den for-Header und ist außerhalb der Schleife unsichtbar.

Typische Fallen

const friert NICHT den Wert ein, nur die Bindung

const arr = [1]; arr.push(2); ist absolut valide und ändert das Array. Wer Wert-Immutability braucht, muss zusätzlich Object.freeze() verwenden — und für tiefe Strukturen einen rekursiven Deep-Freeze. const sagt nur: dieser Name zeigt immer auf denselben Container.

var im Block ist function-scoped — let/const nicht

if (true) { var x = 1 } macht x auch außerhalb des if-Blocks sichtbar. Bei let/const endet die Sichtbarkeit an der schließenden Klammer. In Funktionen mit verschachtelten Blöcken ist das einer der häufigsten Stolpersteine beim Refactoring von altem zu neuem Code.

TDZ ist KEIN Hoisting im klassischen Sinn

let und const werden technisch gehoisted, sind aber im Bereich vor der Deklaration nicht zugreifbar — Zugriff wirft ReferenceError, nicht undefined. Die MDN-Spec spricht hier von „uninitialized binding". Praktisch fühlt es sich an, als wäre die Variable noch nicht da, technisch ist sie es schon.

typeof undeclared ist OK, typeof in TDZ wirft

typeof unbekannt liefert in jedem Modus 'undefined' ohne Error — eine alte Konvention für Feature-Detection. ABER: typeof x innerhalb der TDZ einer let/const-Variablen wirft ReferenceError. Das überrascht regelmäßig: eine Variable, die existiert, fühlt sich „weniger sicher" an als eine, die nicht existiert.

var-Re-Deklaration im Function-Scope ist silently OK

var x = 1; var x = 2; erzeugt keine Warnung und kein Fehler. Bei generiertem Code oder bei zusammengeführten Skript-Dateien führt das zu Bugs, die schwer zu finden sind. let und const werfen sofort SyntaxError beim Parsen.

Globale var-Variablen werden zu Properties auf globalThis

Im klassischen Skript-Modus wird var x = 1 auf Top-Level zu globalThis.x. Das war früher das Standard-Mittel, um Bibliotheken auf window zu exponieren. In modernem Modul-Code (ESM) ist das nicht mehr der Fall — und das ist gut so, weil es Namespace-Pollution verhindert.

const hat KEINE Laufzeit-Performance-Vorteile

Manche Quellen behaupten, const sei „schneller", weil die Engine es optimieren könne. Das stimmt in modernen Engines (V8, SpiderMonkey, JavaScriptCore) nicht: alle drei Deklarationen werden gleich kompiliert. const ist ein semantischer Hinweis für Menschen und Tools (Linter, Type-Checker), kein Performance-Hint.

for-Schleifen mit let erzeugen NEUEN Scope pro Iteration

Das ist die magische Komponente, die setTimeout-Callbacks in Schleifen mit let korrekt funktionieren lässt. Pro Iteration wird ein neuer Block-Scope angelegt, jede Closure schließt über ihre eigene Kopie der Variable. Bei var gibt es nur einen einzigen Slot über die gesamte Funktion — daher die berüchtigten „alle Werte sind 3"-Bugs.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Syntax & Sprachkern

Zur Übersicht