Fortsetzung folgt!
Die angesprochene Besonderheit in Zusammenhang mit dem Operator typeof
zeigt sich wenn versucht wird, den Typ des Wertes einer erst zu einem späteren Zeitpunkt mittels const
oder let
deklarierten Variable zu bestimmen, wenn sich die Variable also in der Temporal Dead Zone befindet. Erinnern wir uns aber zunächst daran, was passiert wenn wir das Gleiche bei einer mit var
deklarierten Variable versuchen.
console.info(typeof Worf); // undefined
var Worf = 'Klingon';
Da mittels var
deklarierte Variablen gehoistet werden, sie also erzeugt und mit dem Wert undefined
initialisiert werden, bevor die Ausführung des mit dem jeweiligen Kontext assoziierten Codes beginnt, wird hier vom Operator typeof
natürlich undefined zurückgegeben. Soweit keine Überraschung. ;-)
Nun sehen wir uns an was passiert, wenn wir mit dem Operator typeof
versuchen den Wert einer Variable zu bestimmen, die überhaupt nicht deklariert wurde:
console.info(typeof Riker); // undefined
Auch hier wird vom Operator typeof
standardgemäß der String 'undefined'
zurückgegeben.
Es wäre also zu erwarten, dass die Prüfung des Wertes einer noch nicht im Code deklarierten const
- oder let
-Variable ebenfalls den Typbezeichner undefined zurückgibt …
function Enterprise ( ) {
// TDZ start
console.info(typeof Data); // Reference Error
// TDZ end
let Data = 'Android';
}
Enterprise( );
… aber wie das Beispiel oben zeigt, fliegt uns statt dessen unser Programm mit einem Reference Error um die Ohren! – Also, was ist hier los? ;-)
Um zu verstehen was hier passiert, müssen wir uns zunächst einmal die Semantik des Operators typeof
näher ansehen, beziehungsweise genauer gesagt, was passiert wenn es sich bei dem Operanden, also dem Ausdruck auf der rechten Seite, um eine Referenz handelt.
// global environment
(function outer ( ) {
// function environment
(function inner ( ) {
// function environment
console.info(typeof LaForge); // undefined
}( ));
}( ));
Handelt es sich wie in dem Beispiel hier bei dem Operanden von typeof
um eine Referenz, dann wird natürlich versucht diese aufzulösen. Das heißt, es wird als erstes in der nächsten lexikalischen Umgebung nachgesehen, ob diese über eine Bindung für den gesuchten Bezeichner verfügt. Das wäre hier also die lexikalische Umgebung, die mit dem Ausführungskontext der Funktion mit dem Bezeichner inner
assoziiert ist.
Da diese lexikalische Umgebung zwar über keine Bindung für den gesuchten Bezeichner verfügt, aber dafür über eine Referenz auf die lexikalische Umgebung des äußeren Kontextes, wird entsprechend dort weitergesucht. Ebenfalls erfolglos. Da schließlich auch in der globalen Umgebung keine Variable oder Funktion mit dem Bezeichner LaForge
deklariert wurde, handelt es sich hier also um eine nicht auflösbare Referenz (Unresolvable Reference). Und für diesen Fall ist standardmäßig vorgesehen, dass von typeof
der Wert 'undefined'
zurückgegeben wird.
Jetzt haben wir aber schon im letzten Teil der Antwort festgestellt, dass Variablen, die mittels const
oder let
deklariert wurden, zwar nicht wie var
-Variablen gehoistet werden, in dem Sinne, dass sie mit einem (Default-)Wert initialisiert werden, aber dass sie eben dennoch vor dem Eintritt in den jeweiligen Scope an die dazugehörige lexikalische Umgebung gebunden werden.
Das heißt, bezogen auf das Ursprungsbeispiel mit Data
handelt es sich bei dem Operanden von typeof
nicht um eine unauflösbare Referenz, denn eine Bindung für diesen Bezeichner in der lexikalischen Umgebung der Funktion Enterprise
besteht ja. Es ist eben nur nicht möglich auf die Variable zuzugreifen, da diese nicht Initialisiert wurde. – Deswegen: Reference Error.
Redeclaration
Ein weiterer Unterschied zwischen solchen Variablen die mittels var
deklariert werden und solchen die mittels const
oder let
deklariert werden besteht im Hinblick auf das Verhalten, das sich zeigt wenn im gleichen Gültigkeitsbereich für den selben Bezeichner mehr als eine Deklaration vorgenommen wird.
var number = 64;
console.log(number); // 64
var number = 128;
console.log(number); // 128
Bei var
-Variablen passiert hier nichts, außer dass bei einer weiteren Deklaration, so denn dabei wie in dem Beispiel ein Wert zugewiesen wird, der alte Wert überschrieben wird. Es wird also bei der Evaluierung des jeweiligen Ausführungskontextes in diesem Fall kein Fehler erzeugt, sondern eben schlicht eine Variablenbindung hinterlegt, es wird also so getan, als wäre die zweite Deklaration nur eine Referenz oder ein Zuweisungsausdruck.
// Type Error before execution
let word = 'foo';
let word = 'foo';
Das sieht bei let
und freilich auch bei const
anders aus. Hier wird sowohl im schlampigen also auch im strikten Ausführungsmodus grundsätzlich eine Ausnahme geworfen.
// Type Error before execution
function foo (bar) {
let bar = 12;
}
Dabei spielt es auch keine Rolle, ob wie in dem Beispiel oben der gleiche Bezeichner verwendet wird wie bei einem formalen Parameter, oder ob im gleichen Gültigkeitsbereich eine Variable mit einem anderen Typ existiert, oder ob dort eine Funktion mit dem entsprechenden Bezeichner deklariert wurde. Eine Redeklaration von const
- und let
-Variablen produziert immer einen Fehler.
Da die mehrfache Verwendung eines Bezeichners für verschiedene Variablen oder Funktionen innerhalb des selben Gültigkeitsbereichs wohl praktisch immer auf eine Nachlässigkeit des Programmierers zurückzuführen ist, ist dieses Verhalten auch durchaus begrüßenswert.
Passiert so etwas mit einer var
-Variablen, dann wird wie gesehen keine Ausnahme geworfen, und die Fehlersuche kann sich unter Umständen als schwierig erweisen, sofern man keine Entwicklungsumgebung verwendet die smart genug ist die Mehrfachdeklaration anzuzeigen.
Zusammenfassend lässt sich also sagen, dass const
und let
im Vergleich zu var
die Befolgung weit strikterer Regeln einfordern, selbst wenn das Programm nicht im im Strict Mode ausgeführt wird, das heißt, durch ihre Verwendung kann eine ganze Reihe potentiell fehlerträchtiger Praktiken von vorneherein ausgeschlossen werden, weshalb ich empfehlen würde, grundsätzlich let
und const
zu verwenden.
Wann nun aber let
und wann const
? ;-)
Naja, der Sinn einer Konstante liegt im Wesentlichen darin, dass es aufgrund der für die gesamte Laufzeit des Programms (beziehungsweise Lebenszeit der Variablen) festgelegte Bindung von Bezeichner und Wert für den Interpreter möglich wird, bestimmte Optimierungen durchzuführen, um die Performanz des Programms zu verbessern.
Wenn also ein Wert hinterlegt werden soll und klar ist, dass die Variable diesen Wert behalten soll, dann verwende ich const
. Ist es hingegen beabsichtigt, dass die Variable zu einem späteren Zeitpunkt einen anderen Wert zugewiesen bekommt, dann verwende ich let
.
const object = {
print ( ) {
console.info(this.value);
}
};
object.value = 42;
object.print( ); // 42
Schließlich handelt es sich bei der Verwendung von const
auch um keine große Einschränkung, denn konstant ist hier tatsächlich nur die Bindung von Bezeichner und Wert, nicht aber der Wert selbst. Das heißt, wenn ein Objekt (und damit auch eine Funktion oder ein Array) als Wert einer Konstante hinterlegt wird, dann kann dieses nach wie vor verändert werden. Auch wenn dadurch vermutlich die Versuche zur Optimierung des Programms durch den Interpreter torpediert werden. ;-)
Hinterlässt
document.body.className = list.includes(value) ? value : '';
bei nicht zutreffender Bedingung nicht unnötigerweise ein leeres Class-Attribut?
Das tut es. Aber du hattest die Änderungen ja nicht von der Präsenz des class
-Attributes auf body
abhängig gemacht, sondern von dessen Wert, weshalb es hier wohl ziemlich egal ist, ob das Attribut gesetzt bleibt oder nicht. Sonst kann man es aber natürlich mittels removeAttribute
entfernen.
Viele Grüße,
Orlok