zurück zum Artikel

Scriptease.js: Umgang von JavaScript mit Variablen

Sprachen

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.

var-Schlüsselwort

var oder nicht var?

Gelegentlich begegnet man der Behauptung, die Verwendung des var-Schlüsselworts sei für globale Variablen nicht erforderlich, und

var x = 23;

außerhalb einer Funktion würde den gleichen Effekt erzielen wie:

x = 23; 

Das ist jedoch falsch. Bei Verwendung von var wird nämlich, wie bereits erwähnt, eine globale Variable angelegt. Entfällt das Schlüsselwort allerdings, wird keine Variable erzeugt, sondern dem globalen Objekt eine Eigenschaft mit dem angegebenen Namen hinzugefügt.

Da Variablendeklarationen intern ebenfalls Eigenschaften erzeugen, ist der effektive Unterschied zwischen diesen beiden Varianten minimal. Gemäß der Sprachspezifikation von ECMAScript verfügt jede Eigenschaft über Attribute, die ihr Verhalten beschreiben, wie ReadOnly oder DontDelete.

Genau in diesem Attribut liegt der Unterschied zwischen einer globalen Variablen und einer Eigenschaft des globalen Objekts: Eine globale Variable wird implizit mit dem Attribut DontDelete versehen, eine Eigenschaft nicht. Rein theoretisch ist es also denkbar, nicht mit var deklarierte "Variablen" mit dem delete-Schlüsselwort wieder zu entfernen:

var x = 23;
y = 42;

delete x;
delete y;

console.log(x);
console.log(y);

Das Löschen der globalen Variablen x schlägt fehl, verursacht allerdings keinen Fehler. Erst der Zugriff auf die beiden Variablen mit der log-Funktion offenbart, dass das Löschen der Variablen x nicht erfolgreich war; im Gegensatz zum Löschen der Variablen y. Während die erste Ausgabe nämlich wie erwartet den Wert 23 ausgibt, verursacht die zweite den bereits erwähnten ReferenceError.

In der Praxis verfügen die beiden Varianten noch über weitere Unterschiede, die unter anderem von der jeweils eingesetzten Laufzeitumgebung abhängen. Daher empfiehlt es sich, zur Deklaration von Variablen stets das Schlüsselwort var zu verwenden, innerhalb von Funktionen ebenso wie außerhalb.

Fazit

Der Umgang mit Variablen in JavaScript ist nicht schwierig, unter Umständen lediglich ein wenig gewöhnungsbedürftig. Wichtig ist, die Bindung des Gültigkeitsbereichs an die gesamte Funktion zu verinnerlichen und das Anheben von Variablendeklarationen zu berücksichtigen. Beides lässt sich auf einfache Art umsetzen, indem der Entwickler sich angewöhnt, sämtliche Variablendeklarationen – anders als eventuell von einer C-Sprache gewohnt – an den Beginn der jeweiligen Funktion zu verschieben. Das vermeidet nicht nur Probleme, sondern entspricht auch der empfohlenen Vorgehensweise für JavaScript.

Anders als häufig behauptet, ist das var-Schlüsselwort nicht optional: Entfällt es, wird anstelle einer Variablen eine Eigenschaft am globalen Objekt erzeugt. Da das in der Regel nicht beabsichtigt ist und zudem ungewollte Seiteneffekte einführt, empfiehlt es sich, niemals eine "Deklaration" ohne var durchzuführen und Code, der das macht, als "falsch" anzusehen. (ane [1])

Golo Roden [2]
ist freiberuflicher Wissensvermittler und Technologieberater für Webentwicklung, Codequalität und agile Methoden.


URL dieses Artikels:
http://www.heise.de/-1417232

Links in diesem Artikel:
[1] mailto:ane@heise.de
[2] http://www.goloroden.de/