14. Mai 2018
Lesedauer 8 Min.
Var, let oder const?
Variablen deklarieren in JavaScript
JavaScript kennt drei Möglichkeiten, Variablen zu deklarieren. Was sind die Unterschiede und welche ist wann zu verwenden?

In der ersten Folge von goloJS [1] wurden verschiedene Besonderheiten des Typsystems von JavaScript erläutert. Der Artikel enthielt im Zusammenhang mit der Frage nach dem Typ von null unter anderem die folgende Aussage:„Die erste Version von JavaScript speicherte jeden Wert [...] in 32 Bit ab. Von diesen 32 Bit wurden die vorderen 3 Bit als Typkennzeichen verwendet [...].“Aus diesen beiden Sätzen lässt sich eine weitere Eigenschaft des Typsystems von JavaScript ableiten, die bislang verschwiegen wurde: Der Typ eines Wertes wird durch den Wert selbst bestimmt, nicht durch die zugehörige Variable. Dieses Vorgehen bildet die Grundlage für die dynamische Natur des Typsystems.Angelehnt ist dieses Verhalten an die Programmiersprache Lisp, bei der ein solches Vorgehen erstmalig verwendet wurde, wie Paul Graham in seinem Blogeintrag unter [2] beschreibt:„4. A new concept of variables.In Lisp, all variables are effectively pointers. Values are what have types, not variables, and assigning or binding variables means copying pointers, not what they point to.“Das Verhalten steht außerdem in direktem Gegensatz zu jenem von C#, in dessen Typsystem der Typ der Variablen zugeordnet ist. Deshalb kennt C# typisierte Variablen, die deklariert, aber nicht definiert sind. In JavaScript ist das so nicht möglich. Deshalb muss die Sprache für deklarierte, aber nicht definierte Variablen auf den Typ undefined zurückgreifen.
Hoisting von Variablen
Das führt zu der Frage, wie eine Variable in JavaScript deklariert werden kann. Tatsächlich stehen zu diesem Zweck drei Schlüsselwörter zur Auswahl, nämlich- var
- let
- const
Das wirkt zunächst harmlos. Da var der Deklaration von lokalen Variablen dient, scheint es nur konsequent, dass der Gültigkeitsbereich auf ebenjene Funktion eingeschränkt ist, in der die Deklaration erfolgt. Die Probleme beginnen, wenn man sich vor Augen führt, dass der Gültigkeitsbereich auch jenen Teil der Funktion einschließt, der vor der Deklaration liegt:
function foo () {
// ...
var x = 23;
// ...
}
Anders als intuitiv zu erwarten, existiert die Variable x nicht erst ab der Zeile, in der sie deklariert und definiert wird. Stattdessen existiert sie bereits direkt zu Beginn der Funktion – ihren Wert erhält sie trotzdem erst in der Zeile, die das Schlüsselwort var enthält, davor ist sie undefined (denn sie wurde zwar deklariert, aber noch nicht definiert).Der Code oben ist daher äquivalent zu dem folgenden:
function foo () {
var x;
// ...
x = 23;
// ...
}
Das beschriebene Verhalten wird als „Hoisting“ bezeichnet (vom englischen Verb „to hoist“, was hochziehen bedeutet). Problematisch dabei ist, dass JavaScript das Hoisting im Hintergrund ausführt, ohne dass der Effekt im Quellcode sichtbar wäre.Als Workaround kann man alle mit var durchgeführten Deklarationen am Anfang der jeweiligen Funktion sammeln. Das macht zumindest den Gültigkeitsbereich von Variablen besser sichtbar, erschwert gleichzeitig aber die Lesbarkeit des Codes.Eine besondere Relevanz gewinnt das Thema in Schleifen, da auch hier das Hoisting greift:
function foo () {
var messages = [ 'Hello', 'world' ];
for (var i = 0; i < messages.length; i++) {
setTimeout(function () {
console.log(messages[i]);
}, i * 1000);
}
}
Der Code gibt – anders als man zunächst erwarten würde – nicht "Hello world“ aus, sondern zweimal undefined, wie der Screenshot in Bild 1 zweifelsfrei belegt.Ein Block (wie eine Schleife) definiert nämlich keinen Gültigkeitsbereich, denn das ist ausschließlich Funktionen vorbehalten. In dem Beispiel gibt es die Variable i also nur ein einziges Mal in der gesamten Funktion und nicht ein Mal pro Iteration.Am Ende der Schleife enthält i den Wert 2, das Array enthält aber lediglich zwei Werte mit den Indizes 0 und 1. Der versuchte Zugriff auf messages[i] führt daher ins Nichts und gibt den Wert undefined zurück. Beheben lässt sich das Problem, indem man den Code der Schleife in eine anonyme Funktion einschließt, sie direkt aufruft und ihr dabei den Wert der jeweiligen Iteration als Parameter übergibt:
function foo () {
var messages = [ 'Hello', 'world' ];
for (var i = 0; i < messages.length; i++) {
(function (value) {
setTimeout(function () {
console.log(messages[value]);
}, value * 1000);
})(i);
}
}
Das Vorgehen funktioniert zwar, führt aber zu äußerst schlecht lesbarem und unnötig verschachteltem Code.
let ist das neue var
Seit der Sprachversion ES2015 gibt es außer var zusätzlich noch das Schlüsselwort let. Es dient ebenfalls der Definition von lokalen Variablen, allerdings gelten für let andere Regeln im Hinblick auf Gültigkeitsbereiche. Zum einen verzichtet JavaScript bei let nämlich auf das Hoisting, zum anderen stellen Blöcke zusätzlich zu Funktionen eigene Gültigkeitsbereiche dar.Ersetzt man var durch let, ergibt sich im einfachsten Fall der folgende Code:function foo () {
// ...
let x = 23;
// ...
}
Anders als zuvor gilt hier, dass die Deklaration ebenso wie die Definition tatsächlich erst in der Zeile stattfindet, in der das Schlüsselwort let verwendet wird. Der Versuch, schon vor der Deklaration auf die Variable x zuzugreifen, führt zu einem „Reference Error“. Das stellt sicher, dass Variablen in jedem Fall zunächst explizit deklariert werden, bevor man auf sie zugreifen kann.Außerdem ist es mit let nicht mehr möglich, eine bereits deklarierte Variable nochmals zu deklarieren. Mit var funktioniert das hingegen problemlos, wobei es lediglich dazu führt, dass die zweite Deklaration ignoriert wird. Eine erneute Deklaration mit let führt hingegen zu einem „Syntax Error“, weil JavaScript davon ausgeht, dass es sich um einen tatsächlichen Programmierfehler handelt.Auch im Beispiel mit der Schleife verhält sich let anders: Zum einen wird die Schleifenvariable nur für den Gültigkeitsbereich innerhalb der Schleife und nicht für die gesamte Funktion deklariert. Zum anderen wird für jede Iteration eine neue Instanz der Schleifenvariable erzeugt, weshalb das vorherige Beispiel einwandfrei funktioniert:
function foo () {
var messages = [ 'Hello', 'world' ];
for (let i = 0; i < messages.length; i++) {
setTimeout(function () {
console.log(messages[i]);
}, i * 1000);
}
}
Außer für <em>for</em>-Schleifen funktioniert das auch für <em>for-in</em>- und die modernen <em>for-of`</em>-Schleifen.
Ein weiterer Unterschied zwischen var und let betrifft den Einsatz des „Strict Mode“ in JavaScript: Das Schlüsselwort let ist nur innerhalb des Strict Mode ein reserviertes Schlüsselwort. Deshalb ist es in altem Code theoretisch möglich, eine Variable namens let zu deklarieren – in modernem Code ist das nicht mehr möglich.Abgesehen von diesen Unterschieden verhält sich let genauso wie var, weshalb man var in nahezu allen Fällen ohne Probleme durch let ersetzen kann. Die Codeanalyse ESLint kennt daher auch die Regel „no-var“, mit der sich der Einsatz von var verbieten und der von let erzwingen lässt [3].
const ist das neue let
ES2015 hat neben let auch const als neues Schlüsselwort eingeführt, das ebenfalls als Ersatz für var dient. Der einzige Unterschied zwischen let und const ist, dass Variablen neu zugewiesen werden können, wenn sie mit let deklariert wurden: const verbietet das.Das bedeutet auch, dass Variablen, die mit const deklariert werden, sofort definiert werden müssen. Während also
let x;
x = 23;
gültiger Code ist, gilt das für
const x;
x = 23;
nicht. In dem Fall müssen Deklaration und Definition zwingend in eine Zeile zusammengezogen werden:
const x = 23;
In der Praxis empfiehlt es sich, const statt let zu verwenden. Auch dafür gibt es eine Regel in ESLint, nämlich „prefer-const“ [4]. Außerdem lässt sich mit der Regel „no-const-assign“ auch bereits durch die Codeanalyse prüfen, dass keine Werte an eine Variable neu zugewiesen werden, die mit const deklariert wurde [5].Als Faustregel für die Verwendung der drei Schlüsselwörter kann man sich zusammenfassend leicht merken:
const > let > var
Übrigens bedeutet const nicht, wie der Name nahelegen könnte, dass es sich um eine Konstante handelt: Es wird lediglich die Bindung der Variablen an ihren Wert als unveränderlich markiert.Verweist eine Variable aber auf einen Referenztyp, beispielsweise ein Objekt oder ein Array, lässt sich dessen Inhalt problemlos verändern – nur die Referenz an sich ist unveränderlich! In dieser Hinsicht verhält sich const also wie das gleichnamige Pendant in C#.
Klassen verhalten sich wie let
Außer let und const hat ES2015 noch viele weitere Schlüsselwörter und Sprachmerkmale eingeführt, unter anderem auch Klassen, wozu das Schlüsselwort class dient.Die Definition einer Klasse mit class verhält sich wie die Deklaration einer Variablen mit let, das heißt, die Bindung des Klassennamens ist veränderbar, auch wenn das in der Praxis zugegebenermaßen nicht allzu sinnvoll ist. Es bedeutet aber auch, dass das erneute Definieren einer Klasse mit dem gleichen Namen einer Redeklaration entspricht, was wie erwähnt zu einem Syntax Error führt.Fazit
Sieht man von altem Code einmal ab, für den ein gutes Verständnis von var und dem damit zusammenhängenden Hoisting relevant ist, braucht man sich im Alltag mit var nicht mehr zu befassen. let und const sind in allen Webbrowsern verfügbar, selbst im Internet Explorer 11 (siehe [6] und [7]). Auch auf der Serverseite sieht die Unterstützung gut aus. So kennt Node.js let seit Version 4.x [8], const partiell ebenfalls seit Version 4.x und vollständig seit 6.x [9]. Abgesehen davon lassen sich die beiden Schlüsselwörter auch durch Transpiler wie Babel [10] in alten Code zurückübersetzen, der auf var basiert.Empfehlenswert ist, die Codeanalyse wie bereits erwähnt so zu konfigurieren, dass const und let favorisiert werden, wobei der Einsatz von const wiederum vorzuziehen ist. Ein Regelsatz für ESLint, der diese Regeln entsprechend umsetzt, steht beispielsweise mit eslint-config-es [11] zur Verfügung. Als Kurzfassung dient die bereits erwähnte Formel:Fussnoten
- Golo Roden, Gleich und doch nicht dasselbe, dotnetpro 5/2018, Seite 60 ff.,
- What Made Lisp Different, http://www.paulgraham.com/diff.html
- no-var, https://eslint.org/docs/rules/no-var
- prefer-const, https://eslint.org/docs/rules/prefer-const
- no-const-assign, https://eslint.org/docs/rules/no-const-assign
- let, https://caniuse.com/#search=let
- const, https://caniuse.com/#search=const
- http://node.green/#ES2015-bindings-let, http://node.green/#ES2015-bindings-let
- http://node.green/#ES2015-bindings-const, http://node.green/#ES2015-bindings-const
- https://babeljs.io, https://babeljs.io
- https://www.npmjs.com/package/eslint-config-es, https://www.npmjs.com/package/eslint-config-es