JavaScript-Funktionsdeklaration und Auswertungsreihenfolge

80

Warum funktioniert das erste dieser Beispiele nicht, aber alle anderen?

// 1 - does not work
(function() {
setTimeout(someFunction1, 10);
var someFunction1 = function() { alert('here1'); };
})();

// 2
(function() {
setTimeout(someFunction2, 10);
function someFunction2() { alert('here2'); }
})();

// 3
(function() {
setTimeout(function() { someFunction3(); }, 10);
var someFunction3 = function() { alert('here3'); };
})();

// 4
(function() {
setTimeout(function() { someFunction4(); }, 10);
function someFunction4() { alert('here4'); }
})();
Wir sind alle Monica
quelle

Antworten:

182

Dies ist weder ein Umfangsproblem noch ein Schließungsproblem. Das Problem liegt im Verständnis zwischen Deklarationen und Ausdrücken .

JavaScript-Code wird in zwei Phasen verarbeitet, da selbst die erste Version von JavaScript von Netscape und die erste Kopie von Microsoft davon verarbeitet werden:

Phase 1: Kompilierung - In dieser Phase wird der Code in einen Syntaxbaum kompiliert (und je nach Engine Bytecode oder Binärcode).

Phase 2: Ausführung - Der analysierte Code wird dann interpretiert.

Die Syntax für die Funktionsdeklaration ist:

function name (arguments) {code}

Argumente sind natürlich optional (Code ist ebenfalls optional, aber wozu dient das?).

Mit JavaScript können Sie jedoch auch Funktionen mithilfe von Ausdrücken erstellen . Die Syntax für Funktionsausdrücke ähnelt Funktionsdeklarationen, außer dass sie im Ausdruckskontext geschrieben sind. Und Ausdrücke sind:

  1. Alles rechts von einem =Zeichen (oder :auf Objektliteralen).
  2. Alles in Klammern ().
  3. Parameter zu Funktionen (dies wird tatsächlich bereits von 2 abgedeckt).

Ausdrücke im Gegensatz zu Deklarationen werden eher in der Ausführungsphase als in der Kompilierungsphase verarbeitet. Und deshalb ist die Reihenfolge der Ausdrücke wichtig.

Um dies zu verdeutlichen:


// 1
(function() {
setTimeout(someFunction, 10);
var someFunction = function() { alert('here1'); };
})();

Phase 1: Zusammenstellung. Der Compiler erkennt, dass die Variable someFunctiondefiniert ist, und erstellt sie. Standardmäßig haben alle erstellten Variablen den Wert undefined. Beachten Sie, dass der Compiler zu diesem Zeitpunkt noch keine Werte zuweisen kann, da der Interpreter möglicherweise einen Code ausführen muss, um einen zuzuweisenden Wert zurückzugeben. Und zu diesem Zeitpunkt führen wir noch keinen Code aus.

Phase 2: Ausführung. Der Interpreter sieht, dass Sie die Variable someFunctionan setTimeout übergeben möchten. Und so ist es auch. Leider ist der aktuelle Wert von someFunctionundefiniert.


// 2
(function() {
setTimeout(someFunction, 10);
function someFunction() { alert('here2'); }
})();

Phase 1: Zusammenstellung. Der Compiler sieht, dass Sie eine Funktion mit dem Namen someFunction deklarieren, und erstellt sie daher.

Phase 2: Der Interpreter sieht, dass Sie someFunctionan setTimeout übergeben möchten. Und so ist es auch. Der aktuelle Wert von someFunctionist die kompilierte Funktionsdeklaration.


// 3
(function() {
setTimeout(function() { someFunction(); }, 10);
var someFunction = function() { alert('here3'); };
})();

Phase 1: Zusammenstellung. Der Compiler sieht, dass Sie eine Variable deklariert haben, someFunctionund erstellt sie. Nach wie vor ist sein Wert undefiniert.

Phase 2: Ausführung. Der Interpreter übergibt eine anonyme Funktion an setTimeout, um sie später auszuführen. In dieser Funktion wird angezeigt, dass Sie die Variable verwenden, someFunctionsodass die Variable geschlossen wird. Zu diesem Zeitpunkt ist der Wert von someFunctionnoch undefiniert. Dann sehen Sie, wie Sie eine Funktion zuweisen someFunction. Zu diesem Zeitpunkt ist der Wert von someFunctionnicht mehr undefiniert. Eine Hundertstelsekunde später wird das setTimeout ausgelöst und die someFunction aufgerufen. Da sein Wert nicht mehr undefiniert ist, funktioniert es.


Fall 4 ist wirklich eine andere Version von Fall 2, in die ein bisschen Fall 3 eingeworfen ist. An dem Punkt, someFunctionan dem setTimeout übergeben wird, existiert es bereits, weil es deklariert wurde.


Zusätzliche Klarstellung:

Sie fragen sich vielleicht, warum setTimeout(someFunction, 10)kein Abschluss zwischen der lokalen Kopie von someFunction und der an setTimeout übergebenen Kopie erstellt wird. Die Antwort darauf ist, dass Funktionsargumente in JavaScript immer, immer als Wert übergeben werden, wenn es sich um Zahlen oder Zeichenfolgen handelt, oder als Referenz für alles andere. SetTimeout erhält also nicht die Variable someFunction, die an ihn übergeben wird (was bedeuten würde, dass ein Abschluss erstellt wird), sondern nur das Objekt, auf das someFunction verweist (was in diesem Fall eine Funktion ist). Dies ist der in JavaScript am häufigsten verwendete Mechanismus zum Aufheben von Verschlüssen (z. B. in Schleifen).

Slebetman
quelle
7
Das war eine wirklich gute Antwort.
Matt Briggs
1
@Matt: Ich habe dies an anderer Stelle (mehrmals) auf SO erklärt. Einige meiner Lieblingserklärungen: stackoverflow.com/questions/3572480/…
slebetman
3
@Matt: Technisch gesehen umfassen Schließungen nicht den Bereich, sondern den Stapelrahmen (auch als Aktivierungsdatensatz bezeichnet). Ein Abschluss ist eine Variable, die von Stapelrahmen gemeinsam genutzt wird. Ein Stapelrahmen soll festlegen, was ein Objekt zu klassifizieren ist. Mit anderen Worten, ein Bereich ist das, was der Programmierer in der Codestruktur wahrnimmt. Ein Stapelrahmen wird zur Laufzeit im Speicher erstellt. Es ist nicht wirklich so, aber nah genug. Wenn Sie über das Laufzeitverhalten nachdenken, reicht ein bereichsbasiertes Verständnis manchmal nicht aus.
Slebetman
3
@slebetman Für Ihre Erklärung von Beispiel 3 erwähnen Sie, dass die anonyme Funktion in setTimeout einen Abschluss für die Variable someFunction erstellt und dass someFunction zu diesem Zeitpunkt noch undefiniert ist - was sinnvoll ist. Es scheint, dass der einzige Grund, warum Beispiel 3 nicht undefiniert zurückgibt, die Funktion setTimeout ist (die Verzögerung von 10 Millisekunden ermöglicht es JavaScript, die nächste Zuweisungsanweisung an someFunction auszuführen, wodurch sie definiert wird), oder?
Wmock
2

Der Umfang von Javascript basiert auf Funktionen und nicht ausschließlich auf lexikalischem Geltungsbereich. das bedeutet, dass

  • Somefunction1 wird vom Beginn der umschließenden Funktion definiert, aber sein Inhalt ist bis zur Zuweisung undefiniert.

  • Im zweiten Beispiel ist die Zuweisung Teil der Deklaration, sodass sie nach oben verschoben wird.

  • Im dritten Beispiel ist die Variable vorhanden, wenn der anonyme innere Verschluss definiert ist. Sie wird jedoch erst 10 Sekunden später verwendet. Bis dahin wurde der Wert zugewiesen.

  • Das vierte Beispiel hat sowohl den zweiten als auch den dritten Grund zu arbeiten

Javier
quelle
1

Weil someFunction1zum Zeitpunkt der setTimeout()Ausführung des Anrufs noch nicht zugewiesen wurde .

someFunction3 kann wie ein ähnlicher Fall aussehen, aber da Sie vorbei eine Funktion Verpackung someFunction3()zu setTimeout()dem Anruf in diesem Fall zu someFunction3()erst später ausgewertet.

matt b
quelle
Wurde someFunction2aber noch nicht zugewiesen, wenn der Aufruf von setTimeout()ausgeführt wird ...?
Wir sind alle Monica
1
@jnylen: Das Deklarieren einer Funktion mit dem functionSchlüsselwort entspricht nicht genau dem Zuweisen einer anonymen Funktion zu einer Variablen. Als deklarierte Funktionen function foo()werden an den Anfang des aktuellen Bereichs "gehisst", während Variablenzuweisungen an dem Punkt erfolgen, an dem sie geschrieben werden.
Chuck
1
+1 für spezielle Funktionen. Nur weil es funktionieren kann , heißt das noch lange nicht, dass es getan werden sollte. Immer deklarieren, bevor Sie verwenden.
Mway
@mway: In meinem Fall habe ich meinen Code innerhalb einer "Klasse" in Abschnitte unterteilt: private Variablen, Ereignishandler, private Funktionen, dann öffentliche Funktionen. Ich benötige einen meiner Event-Handler, um eine meiner privaten Funktionen aufzurufen. Wenn ich den Code so organisiert halte, gewinnt das für mich, wenn ich die Deklarationen lexikalisch ordne.
Wir sind alle Monica
1

Dies klingt wie ein grundlegender Fall, wenn Sie ein gutes Verfahren befolgen, um Ärger zu vermeiden. Deklarieren Sie Variablen und Funktionen, bevor Sie sie verwenden, und deklarieren Sie Funktionen wie folgt:

function name (arguments) {code}

Vermeiden Sie es, sie mit var zu deklarieren. Dies ist nur schlampig und führt zu Problemen. Wenn Sie es sich zur Gewohnheit machen, alles zu deklarieren, bevor Sie es verwenden, werden die meisten Ihrer Probleme in großer Eile verschwinden. Wenn ich Variablen deklariere, würde ich sie sofort mit einem gültigen Wert initialisieren, um sicherzustellen, dass keine von ihnen undefiniert ist. Ich neige auch dazu, Code einzuschließen, der nach gültigen Werten globaler Variablen sucht, bevor eine Funktion sie verwendet. Dies ist ein zusätzlicher Schutz vor Fehlern.

Die technischen Details, wie das alles funktioniert, ähneln der Physik, wie eine Handgranate funktioniert, wenn Sie damit spielen. Mein einfacher Rat ist, überhaupt nicht mit Handgranaten zu spielen.

Einige einfache Deklarationen am Anfang des Codes lösen möglicherweise die meisten dieser Probleme, aber eine Bereinigung des Codes ist möglicherweise noch erforderlich.

Zusätzlicher Hinweis:
Ich habe einige Experimente durchgeführt und es scheint, dass es keine Rolle spielt, in welcher Reihenfolge sie alle Ihre Funktionen auf die hier beschriebene Weise deklarieren. Wenn Funktion A Funktion B verwendet, muss Funktion B dies nicht tun vor Funktion A deklariert werden.

Deklarieren Sie also zuerst alle Ihre Funktionen, dann Ihre globalen Variablen und setzen Sie dann Ihren anderen Code zuletzt ein. Befolgen Sie diese Faustregeln und Sie können nichts falsch machen. Es ist möglicherweise sogar am besten, Ihre Erklärungen in den Kopf der Webseite und Ihren anderen Code in den Text einzufügen, um die Durchsetzung dieser Regeln sicherzustellen.

Terry Prothero
quelle