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.
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| Eigenschaft | var | let | const |
|---|---|---|---|
| eingeführt mit | ES1 (1997) | ES2015 | ES2015 |
| Scope | Function / Global | Block | Block |
| Hoisting | ja, init. mit undefined | ja, aber TDZ | ja, aber TDZ |
| Re-Assignment | erlaubt | erlaubt | verboten (TypeError) |
| Re-Deklaration | erlaubt (silently) | SyntaxError | SyntaxError |
| Top-Level → globalThis | ja (Browser-Global) | nein | nein |
| Initialisierung Pflicht | nein | nein | ja |
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.
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[ 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.
// const x; // SyntaxError: Missing initializer in const declaration
const x = undefined; // technisch ok, aber kaum sinnvolllet 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.
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)); // 13580
135Der 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).
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'undefined
5
halloIn 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.
// 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: 2var: 3
var: 3
var: 3
let: 0
let: 1
let: 2Der 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.
{
// 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.
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();undefined
1Die 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.
// 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; // SyntaxErrorDer 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.
// 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); // undefined1
undefined
undefinedIn 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:
constist der Default. Solange die Variable im Lauf ihres Lebens nicht neu zugewiesen wird, istconstdie richtige Wahl. Das Lesen wird einfacher, weil einconstdem Leser signalisiert: dieser Name zeigt von hier an immer auf denselben Wert.letnur, wenn Re-Assignment nötig ist. Schleifen-Variablen, Akkumulatoren infor-Loops, Flags, die ihren Zustand wechseln. Wenn sich beim Refactoring zeigt, dass dasletdoch nicht reassignt wird, durchconstersetzen.varpraktisch 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 istvarein Geruch.
Praxis: Refactoring von var zu let/const
Eine typische Funktion aus einer Pre-2015-Codebasis und ihre moderne Form:
// 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;
}// 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][ 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.