Hallo MB,
das ist jetzt Herumreiten auf Kleinigkeiten, aber ich glaube, genau die interessieren Dich. Tom - bitte weggucken. Buzzwordsturm im Anzug!
Hoisting kennst Du - als das "Hochziehen" einer Deklaration an den Anfang des Bereichs, in dem sie stattfindet.
Ohne Hoisting wäre
let x = foo(7);
function foo(a)
{
return a+1;
}
ein Fehler, weil foo im Moment des Aufrufs noch nicht deklariert ist. Sprachen wie C oder C++ würden Dir diesen Code abweisen. Was daran liegt, dass C für eine Welt geschaffen wurde, in der ein Two-Pass Compiler unanständiger Luxus war. Aber die "Declare before Use" Forderung hat sich dort gehalten.
"Deklarieren" bedeutet dabei, dem Compiler mitzuteilen, dass dieser Name einer ist, den ich verwenden will, so dass der Compiler weiß, dass foo
kein Tippfehler ist. Je nach Sprache teile ich dem Compiler auch gleich mit, wofür ich den Namen verwenden will - was man in JavaScript aber nicht tut.
Das Zuordnen eines Wertes zu diesem Namen ist dann das "Definieren". Die Besonderheit des function Statements ist, dass es eine Funktion nicht nur deklariert, sondern auch gleich definiert. Bei Variablen wird die Deklaration zwar gehoistet, aber die Definition erfolgt an der Stelle, wo das var, const oder let Statement steht.
let und const werden gehoistet? Eigentlich nicht, und eigentlich doch. Gleich mehr dazu.
Und - Compiler? Ja, sicher. JavaScript übersetzt den Sourcecode erstmal komplett, bevor es ihn ausführt, in einen Bytecode (die "Creation Phase" eben). Ältere JS Engines haben den Bytecode dann interpretiert. Neuere Engines wie Google V8 oder Firefox Spidermonkey übersetzen den Bytecode einer Funktion vor ihrer ersten Ausführung erstmal in Maschinencode. Mozilla erzählt ganz stolz, dass sie das besonders geschickt machen: erstmal "grob" übersetzen, und wenn sie feststellen, dass eine Funktion öfter aufgerufen wird, läuft ein optimierender Compiler im Hintergrund nochmal drüber.
Das Hoisting hat so seine Merkwürdigkeiten. Wenn ich einen Namen doppelt benutzer, für var und function, meckert JS nicht. Aber die Funktion wird ignoriert. Egal, wie herum es aufgeschrieben ist. Sudhakar Pilavadi schreibt zwar, dass bei einem Namenskonflikt Funktionen Vorrang vor Variablen hätten, aber mein Chrome tut das Gegenteil.
var x = 7;
function x() { };
console.log(x); // 7
Ich sehe die 7, egal ob das var vor oder hinter dem function statement steht.
Wie gesagt: das function
Statement deklariert UND definiert, deswegen kann im Eingangsbeispiel die foo
-Funktion schon vor dem function
Statement verwendet werden.
In einem var
Statement wird dagegen nur der Name bekannt gemacht. Die Zuweisung des Wertes erfolgt an der Stelle, wo das var
notiert ist:
console.log(foo(2));
var foo = function(a) {
return a + 1;
};
Der Variablenname foo
ist in Zeile 1 bereits bekannt, aber die function expression ist noch nicht zugewiesen. Deswegene enthält foo
in der Zeile 1 noch den Wert undefined
und es gibt eine Fehlermeldung.
Bei const
und let
ist es strenger. Auch hier werden die Namen im ganzen Scope bekannt gemacht, aber eine Wiederverwendung dieser Namen ist verboten. Ein Name, der mit const
oder let
deklariert wurde, kann nicht mehr etwas anderes verwendet werden, und umgekehrt kann mit const
oder let
kein anderweit deklarierter Name redeklariert werden.
Hinzu kommt, dass ein mit const
oder let
deklarierte Name erst "angefasst" werden darf, wenn das const
/let
Statement durchlaufen wurde. JavaScript kennt den Namen durchaus schon vorher.
console.log("huhu");
console.log(foo);
gibt "huhu" aus und meckert dann mit einem ReferenceError, dass "foo" undefiniert sei. Hier dagegen
console.log("huhu");
console.log(foo);
const foo = 12;
kommt nach dem "huhu" ebenfalls ein ReferenceError, aber mit dem Text, dass foo vor seiner Initialisierung verwendet würde.
Wenn Du also sowas baust:
foo(12);
const foo = function(x) {
return x+1;
}
dann zwingst Du Dich dazu, deine Funktionen hinzuschreiben, bevor Du sie benutzt.
Und an dieser Stelle bin ich durchaus anderer Meinung als Matthias. Es ist nämlich aus meiner Sicht durchaus sinnvoll, Code "top down" aufzuschreiben. Wenn ich ein Programm entwerfe, und wenn ich ein fremdes Programm verstehen will, gehe ich top-down vor. Ich überlege mir (oder schaue mir an), was das Programm auf oberster Ebene tut, und wenn die Funktionen, die dort aufgerufen werden, gute Namen tragen, muss ich mir ihre Implementierung nicht anschauen, um zu wissen, was auf oberster Ebene passiert. Wenn ich Dinge genauer wissen muss, steige ich in die zweite Ebene ein, und so weiter.
Code, der zunächst einmal eine Geröllhalde aus kleinteiligen Funktionen daherkübelt und erst am Ende "zur Sache" kommt, muss ich von unten nach oben lesen, um ihn zu verstehen. Und das finde ich viel unübersichtlicher. Bzw. wenn ich selbst programmiere und erst einmal einen Haufen Kroppzeug als unterste Ebene zusammenprogrammiere, schreibe ich vermutlich eine Menge Code auf Vorrat, den ich nachher eventuell gar nicht brauche.
Beim top-down Ansatz kann ich manche Funktionen zumächst als Stub programmieren und Dummy-Werte zurückliefern, und die oberste Ebene sauber gestalten, bevor ich in die Einzelheiten gehe. Aber das mag eine Frage persönlichen Stils sein.
Rolf
sumpsi - posui - obstruxi