Scriptease.js: Umgang von JavaScript mit Variablen
Mit Variablen in JavaScript klarzukommen, ist gewöhnungsbedürftig: Der auf Funktionen bezogene Gültigkeitsbereich von Variablen und das Anheben von Variablendeklarationen stiften zumindest anfänglich durchaus Verwirrung. heise Developer erläutert, worauf es bei der Verwendung von Variablen zu achten gilt.
- Golo Roden
Mit Variablen in JavaScript klarzukommen, ist gewöhnungsbedürftig: Der auf Funktionen bezogene Gültigkeitsbereich von Variablen und das Anheben von Variablendeklarationen stiften zumindest anfänglich durchaus Verwirrung. heise Developer erläutert, worauf es bei der Verwendung von Variablen zu achten gilt.
Die meisten, sich von C ableitenden Sprachen, etwa C# oder Java, verwenden sogenannte statische Gültigkeitsbereiche zum Auflösen von Variablen. Sie bewirken, dass die Sprachen die Variablen in ihrem, bereits zur Übersetzungszeit bekannten, lexikalischen Umfeld auflösen, das sich aus der Struktur des Quelltexts ergibt. Deshalb werden statische häufig auch als lexikalische Gültigkeitsbereiche bezeichnet.
Was steckt hinter Scriptease.js?
JavaScript erlernt sich deutlich leichter als die meisten anderen Programmiersprachen. Dennoch ist die Sprache nicht per se einfach, ganz im Gegenteil. Die Palette reicht dabei von exotischen und selten verwendeten Schlüsselwörtern bis hin zu unerklärlichem und scheinbar widersprüchlichem Verhalten. Die Kolumne Scriptease.js führt in die Feinheiten von JavaScript ein und vermittelt sowohl subtile Eigenarten der Sprache als auch bemerkenswerte Interna.
Im Gegensatz dazu steht die Verwendung dynamischer Gültigkeitsbereiche, die Variablen zur Laufzeit in Abhängigkeit der Reihenfolge der aufrufenden Funktionen auflösen. Dass Verfahren findet sich unter anderem in Lisp und Scheme.
Funktionsbezogener GĂĽltigkeitsbereich
Die meisten Sprachen mit lexikalischem GĂĽltigkeitsbereich beziehen diesen dabei auf die Ebene eines Anweisungsblocks, den in C-Sprachen in der Regel ein Paar geschweifter Klammern einschlieĂźt. Das folgende, in C# geschriebene Beispiel demonstriert das Verhalten:
int x = 23;
if(x == 23)
{
int y = 42;
Console.WriteLine("x and y: {0}, {1}", x, y);
}
Console.WriteLine(x);
Console.WriteLine(y);
Der Versuch, das Beispiel zu ĂĽbersetzen, fĂĽhrt aufgrund der letzten Zeile zum Compilerfehler CS0841, laut dem eine Variable (in diesem Fall y) vor ihrer Verwendung zu deklarieren ist.
JavaScript verhält sich anders: Die Skriptsprache bindet den Gültigkeitsbereich einer Variablen nicht an einen Block, sondern an die die Variable deklarierende Funktion. Das vorige Beispiel, übersetzt nach JavaScript, demonstriert das Verhalten:
var x = 23;
if(x === 23) {
var y = 42;
console.log("x and y: " + x + ", " + y);
}
console.log(x);
console.log(y);
Obwohl das Beispiel die Variable y innerhalb des if-Blocks deklariert, ist sie auch auĂźerhalb dieses Blocks verfĂĽgbar, weshalb die letzte Zeile den Wert 42 ausgibt.
JavaScript verhält sich hinsichtlich des lexikalischen Gültigkeitsbereichs jedoch noch in anderer Hinsicht anders als C#: Lokale Variablen sind nicht nur in der sie deklarierten Funktion verfügbar, sondern auch in allen, in dieser Funktion deklarierten Funktionen:
function foo() {
var x = 23;
function bar() {
console.log(x);
}
bar();
}
foo();
Die Funktion foo stellt dabei eine lexikalische Closure für die Funktion bar dar. Das bedeutet, dass sich auf den Stackframe der äußeren Funktion auch von der inneren Funktion zugreifen lässt, sodass man lokale Variablen der äußeren Funktion dort ebenfalls verwenden kann. Allerdings können Funktionen bei Bedarf Variablen der Closure durch die Deklaration gleichnamiger lokaler Variablen verbergen. Das folgende Beispiel gibt daher nicht den Wert 23, sondern 42 aus:
function foo() {
var x = 23;
function bar() {
var x = 42;
console.log(x);
}
bar();
}
foo();
Wird eine Variable weder in der gegenwärtigen Funktion noch in einer ihrer Closures noch im globalen Gültigkeitsbereich gefunden, löst eine JavaScript-Laufzeitumgebung wie Node.js schließlich einen ReferenceError aus.
Anheben von Variablendeklarationen
Versteht der JavaScript-Entwickler die Bindung des Gültigkeitsbereichs von Variablen an die Funktion und die Closures, ist das allerdings nur die halbe Miete. Die andere Hälfte besteht im Verständnis des sogenannten "Hoistings" von Variablendeklarationen, wobei sich das Wort Hoisting im Deutschen als "Anheben" wiedergeben lässt. Tatsächlich führt JavaScript genau das mit Variablendeklarationen durch: Die Anweisung
Deklaration vs. Definition
Die beiden Begriffe "Deklaration" und "Definition" werden gerne verwechselt, da viele die beiden Konzepte häufig gemeinsam verwenden – in der Regel sogar in einem einzigen Ausdruck. Als "Deklaration" bezeichnet man den Vorgang, dem Übersetzer oder der Laufzeitumgebung eine Variable bekannt zu machen, sodass sie sich im weiteren Code nutzen lässt. Diese Aufgabe übernimmt in JavaScript die var-Anweisung. Als "Definition" wird hingegen der Vorgang bezeichnet, einer Variablen einen Wert zuzuweisen. Das erfolgt in JavaScript mit dem Zuweisungsoperator =. Es ist technisch gesehen also durchaus möglich, eine Variable lediglich zu deklarieren, nicht aber zu definieren – jedoch nicht umgekehrt.
var x = 23;
wird intern in zwei Anweisungen aufgespalten, von denen die erste die Deklaration und die zweite die Definition enthält (siehe Kasten "Deklaration vs. Definition"):
var x;
x = 23;
Die Besonderheit des Anhebens von Variablendeklarationen in JavaScript besteht nun darin, dass die erste Zeile an den Anfang der umgebenden Funktion verschoben wird. Daher sind die beiden Funktionen
function foo() {
bar();
var x = 23;
}
und
function foo() {
var x;
bar();
x = 23;
}
nicht nur gleichwertig, sondern zur Laufzeit tatsächlich identisch: Die JavaScript-Laufzeitumgebung wandelt die erste in die zweite Version um. Dieses Verhalten von JavaScript ist der Grund dafür, dass eine in einer Funktion deklarierte Variable nicht erst ab der Zeile der Deklaration verfügbar ist, sondern in der gesamten Funktion. Das bedeutet insbesondere, dass Variablen der Closure bereits vor der – scheinbaren – Deklaration verborgen werden. So gibt das Beispiel
function foo() {
var x = 23;
function bar() {
console.log(x);
var x = 42;
}
bar();
}
foo();
anders, als eventuell erwartet, nicht den Wert 23 aus, sondern undefined, da die Deklaration der Variablen x in der Funktion bar angehoben wird. Weil der Standardwert für Variablen in JavaScript undefined ist, wird dieser Wert im Anschluss ausgegeben. Dieses Verhalten begründet außerdem auch die Empfehlung, sämtliche in einer Funktion genutzten Variablen in JavaScript gleich zu Beginn der jeweiligen Funktion zu deklarieren. In den meisten, sich von C ableitenden Sprachen hingegen gilt als guter Stil, Variablen erst dort zu deklarieren, wo sie tatsächlich benötigt werden.
Deklariert der Entwickler eine Variable außerhalb einer Funktion, spricht man in JavaScript von einer globalen Variable, auf die sich implizit aus allen Funktionen zugreifen lässt. Genau genommen handelt es sich bei einer globalen ebenfalls um eine lokale Variable, die sich allerdings im globalen Gültigkeitsbereich befindet, der allen Funktionen implizit als Closure zur Verfügung steht.