1UnitedPower: Anfängerfragen zum Thema Funktionen in JS

Beitrag lesen

Meine Damen und Herren, habe ich Ihre Aufmerksamkeit?

Die Frage ist jetzt:

Was passiert, wenn die Funktion ausgeführt ist, das heißt, wenn ich das Buch gelesen habe?

Das ist schon keine Anfägerfrage mehr.
Ein laufendes JavaScript-Programm hat einen sogenannten Event-Loop. Der besteht vorallem aus zwei Verwaltungs-Strukturen: die asynchrone Event-Queue und der Callstack.

Jeder Funktionsaufruf sorgt dafür, dass unittelbar ein neues Element auf den Callstack gelegt wird. In diesem Element werden allerlei Meta-Informationen über den Funktionsaufruf abelegt, zum Beispiel lokale Variablen, Parameter und ein Programm-Zähler, damit der Interpreter immer genau weiß, welche Anweisung er gerade ausführt. Wenn die Funktion abgearbeitet ist, dann wird das Element wieder vom Callstack gelöscht.

Wenn ein Funktionsaufruf intern einen eine weitere Funktion aufruft, wird für diesen inneren Funktionsaufruf sofort ein weiteres Element auf den Callstack gelegt usw. usw. Irgendwann werden diese verschachtelten Funktionsaufrufe der Reihe nach wieder terminieren und der Stapel leert sich allmählich wieder.

Wenn zwischenzeitlich ein asynchrones Ereignis eintritt, zum Beispiel ein Klick auf einen Button, oder eine Antwort auf ein AJAX-Request, dann unterbricht der Browser nicht die gegenwärtig ausgeführten Funktionen und er legt auch kein neues Element auf den Callstack, er arbeitet den aktuellen Callstack ganz gewöhnlich und ohne Unterbrechung ab. Der Browser vergisst den Klick aber auch nicht, stattdessen reiht er einen Auftrag in die Event-Queue ein. Und wenn der Callstack irgendwann mal wieder leer ist, schaut der Browser in der Event-Queue nach, nimmt sich dort den nächsten Einträg und arbeitet wieder den ganzen Stapel ab.

Ein Beispiel:

function a () { b(); }  
function b () { c(); }  
function c () { alert('whoop'); }  
  
a();

Das Programm ruft erst die Funktion a auf, diese ruft intern b auf, diese wiederum ruft c auf und c ruft schlussendlich alert('whoop'); auf. alert('whoop') ist irgendwann fertig und gibt die Kontrolle an c zurück. c gibt die Kontrolle später zurück an b, b gibt sie zurück an a und irgendwann sagt auch a ich bin fertig.

Wenn wir dieser sprachlichen Ausführung folgen, dann sieht der Callstack dabei ungefähr so aus:

betrete a()

+---+  
| a |  
+---+

betrete b()

+---+  
| b |  
+---+  
| a |  
+---+

betrete c()

+---+  
| c |  
+---+  
| b |  
+---+  
| a |  
+---+

betrete alert('whoop')

+-------+  
| alert |  
+-------+  
| c |  
+---+  
| b |  
+---+  
| a |  
+---+

verlasse alert('whoop')

+---+  
| c |  
+---+  
| b |  
+---+  
| a |  
+---+

verlasse c()

+---+  
| b |  
+---+  
| a |  
+---+

verlasse b()

+---+  
| a |  
+---+

verlasse a()
leerer Stapel

Wichtig an dieser Stelle ist, dass die Elemente oben auf dem Callstack keinen Zugriff auf die Variablen in den Funktionen weiter unten im Callstack haben. Dies gilt auch andersherum.

Im nächsten Beispiel schauen wir uns mal ein Programm an, dass auch die Event-Queue benutzt. Dazu brauchen wir als erstes einen asynchronen Funktionsaufruf. Wir könnten uns einen Klick vorstellen, aber wir bedienen uns der einfachsten aller Möglichkeiten: setImmediate(). Die Funktion nimmt eine Callback-Funktion als Parameter entgegen und macht nichts anderes als diese "asynchron" aufzurufen. Das heißt, die Callback-Funktion wird nicht sofort auf den Callstack gelegt, sondern in die Event Queue eingereiht:

function a() {  
   setImmediate( b );  
}  
  
function b() {  
   alert( 'whoop' );  
}  
  
a();

Wir schauen uns wie vorher den Callstack schrittweise an:

betrete a():
Callstack:

+---+  
| a |  
+---+

Event-Queue: leer

betrete setImmediate():
Callstack:

+--------------+  
| setImmediate |  
+---+----------+  
| a |  
+---+

Event-Queue: b

verlasse setImmediate():
Callstack:

+---+  
| a |  
+---+

Event-Queue: b

verlasse a()
Callstack: leer
Event-Queue: b

betrete b():
Callstack:

+---+  
| b |  
+---+

Event-Queue: leer

betrete alert('whoop')
Callstack:

+-------+  
| alert |  
+---+---+  
| b |  
+---+

Event-Queue: leer

verlasse alert('whoop')
Callstack:

+---+  
| b |  
+---+

Event-Queue: leer;

verlasse b()
Callstack: leer
Event-Queue: leer

Besonders interessant wird es dann, wenn man beginnt Funktions-Definitionen zu verschachteln. Zum Beispiel mit einem Closure.
Wir schreiben eine Funktion, die eine Zahl "a" als Parameter entgegen nimmt und eine neue Funktion zurück gibt, die eine zweite Zahl "b" nimmt und das Ergebnis zurück gibt.

function plus ( a ) {  
   return function ( b ) {  
      return a + b;  
   }  
}  
  
var plus5 = plus(5);  
plus5(3); // 8  
var plus7 = plus(7);  
plus7(3); // 10  
plus7(5); // 12

Versuch dir vorzustellen, wie der Callstack nach jeder Anweisung aussieht.

Wenn wir plus(5) betreten liegt nur ein Element auf dem Callstack, dann verlassen wir plus(5) wieder, der Stapel ist anschließend also wieder leer.

Dann betreten wir den nächsten Funktionsaufruf plus5(3), es liegt wieder nur ein einziges Element auf dem Callstack, nämlich wieder nur der aktuelle Funktionsaufruf. Wie ich am Anfang geschrieben habe, werden auf dem Callstack die lokalen Variablen und Parameter gespeichert, aber der Parameter "a" gehört ja gar nicht zum aktuellen Funktion, sondern zur Funtion plus. Und diese ist zu diesem Zeitpunkt ja längst abgearbeitet. Der Browser hat sich in einem geheimen Gedächtnis trotzdem irgendwo gemerkt, dass der Parameter "a" zuletzt 5 war.

Man nennt diese Programmier-Konstrukt einen Closure. Ein Closure ermöglicht es also Variablen und Parameter über den eigenen Ausführungskontext hinaus, für spätere Funktionsaufrufe zu merken.

Das ist in JavaScript ein beliebtes Konzept und wird sehr viel eingesetzt, es stellt aber auch sehr besondere Ansprüche an die interne Speicherverwaltung. Woher soll der Browser wissen, wann er eine Variable entgültig wegwerfen darf?

Müsste ich das Buch dann nicht wieder zurück ins Regal stellen, sprich die Funktion per Befehl für beendet erklären, damit sie nicht im Speicher bleibt und dessen Kapazität beansprucht?

Wie ich gerade erklärt habe, kann eine Funktion auch noch Speicher belegen, wenn sie längst abgelaufen ist. Das ist ein wichtiges Feature von JavaScript. Die Entscheidung, wie lange welche Variablen vorgehalten werden, trifft in JavaScript nicht der Programmierer, sondern die eingebaute Speicher-Verwaltung.

Oder passiert das automatisch?

Jap, undzwar hauptsächlich durch die Garbage-Collection.

Oder hängt es vom Inhalt der Funktion ab?

Auch das.

Wenn ich also über das DOM die einzelnen Elemente meiner Seite steuere, indem ich Funktionen nach dem Schema meines Code-Beispiels verwende, muss ich die dann irgendwie wieder "schließen", oder werden solche Funktionen nach vollbrachter Tat automatisch wieder "zurück ins Regal gestellt", und verbrauchen keinen Speicher mehr?

Der JavaScript-Interpreter macht weiter im Text, aber er reserviert vielleicht trotzdem noch Speicher für vergangene Funktionsaufrufe, vielleicht auch unnötigen Speicher, wer weiß.

Überhaupt, wie verhält sich das mit "inaktiven" Funktionen - welchen Einfluss übt deren schieres Vorhandensein im script auf die Performance einer Seite aus?

Ja in kaum messbaren Bereichen. Darum solltest du dir keine Sorgen machen müssen. Entwickle dein Programm als erstes nur unter software-architektkonischen Gesichtspunkten und ignoriere Speicher-Performanz erst mal. Es ist unwahrscheinlich, dass das später zu deinem Flaschenhals wird.

Wenn dein Programm später unter Speicherproblemen leidet, dann analysiere die Ursache und gehe gezielt dagegen vor, anstatt von vornerein an allen Ecken und Enden kleine Tweaks vorzunehmen.

Da meine js-Seitensteuerung ziemlich umfangreich zu werden droht, mache ich mir so meine Gedanken über eine möglichst speicherschonende Programmierung...

Mach dir die Gedanken, wenn du wirklich Probleme damit kriegst, nicht vorher.

--
“All right, then, I'll go to hell.” – Huck Finn