Get to know MDN better
Dieser Inhalt wurde automatisch aus dem Englischen übersetzt, und kann Fehler enthalten. Erfahre mehr über dieses Experiment.
Diese Seite führt in die grundlegende Infrastruktur der JavaScript-Laufzeitumgebung ein. Das Modell ist größtenteils theoretisch und abstrakt, ohne plattform- oder implementierungs-spezifische Details. Moderne JavaScript-Engines optimieren die beschriebenen Semantiken stark.
Diese Seite ist eine Referenz. Sie setzt voraus, dass Sie bereits mit dem Ausführungsmodell anderer Programmiersprachen wie C und Java vertraut sind. Sie bezieht sich stark auf bestehende Konzepte in Betriebssystemen und Programmiersprachen.
Für die Ausführung von JavaScript ist die Zusammenarbeit von zwei Software-Komponenten erforderlich: der JavaScript-Engine und die Host-Umgebung.
Die JavaScript-Engine implementiert die ECMAScript (JavaScript) Sprache, welche die Kernfunktionalität bereitstellt. Sie nimmt Quellcode, analysiert ihn und führt ihn aus. Um jedoch mit der Außenwelt zu interagieren, beispielsweise um aussagekräftige Ausgaben zu erzeugen, externe Ressourcen zu nutzen oder sicherheits- oder leistungsbezogene Mechanismen zu implementieren, benötigen wir zusätzliche, umgebungsspezifische Mechanismen, die von der Host-Umgebung bereitgestellt werden. Zum Beispiel ist der HTML DOM die Host-Umgebung, wenn JavaScript in einem Webbrowser ausgeführt wird. Node.js ist eine weitere Host-Umgebung, die es JavaScript ermöglicht, serverseitig ausgeführt zu werden.
Während wir uns in dieser Referenz hauptsächlich auf die in ECMAScript definierten Mechanismen konzentrieren, werden wir gelegentlich über Mechanismen sprechen, die in der HTML-Spezifikation definiert sind und oft von anderen Host-Umgebungen wie Node.js oder Deno nachgeahmt werden. So können wir ein kohärentes Bild des JavaScript-Ausführungsmodells geben, wie es im Web und darüber hinaus verwendet wird.
In der JavaScript-Spezifikation wird jeder autonome JavaScript-Ausführer als Agent bezeichnet, der seine eigenen Mittel zur Ausführung von Code unterhält:
Dies sind drei verschiedene Datenstrukturen, die unterschiedliche Daten im Auge behalten. Wir werden die Warteschlange und den Stapel in den folgenden Abschnitten genauer vorstellen. Um mehr darüber zu lesen, wie Speicherplatz im Heap zugewiesen und freigegeben wird, lesen Sie Speicherverwaltung.
Jeder Agent ist analog zu einem Thread (beachten Sie, dass die zugrunde liegende Implementierung möglicherweise nicht tatsächlich ein Betriebssystem-Thread ist). Jeder Agent kann mehrere Realm besitzen (die 1-zu-1 mit globalen Objekten korrelieren), die sich gegenseitig synchron aufrufen können und dadurch in einem einzelnen Ausführungsthread laufen müssen. Ein Agent hat auch ein einzelnes Speicher-Modell, das angibt, ob es little-endian ist, ob es synchron blockiert werden kann, ob atomare Operationen sperrfrei sind, etc.
Ein Agent im Web kann eine der folgenden Formen annehmen:
Mit anderen Worten, jeder Arbeiter erstellt seinen eigenen Agenten, während eines oder mehrere Fenster im selben Agent sein können—normalerweise ein Hauptdokument und seine ähnlichen Ursprungs-Iframes. In Node.js steht ein ähnliches Konzept namens worker threads zur Verfügung.
Das nachstehende Diagramm illustriert das Ausführungsmodell von Agenten:
Jeder Agent besitzt ein oder mehrere Realms. Jedes Stück JavaScript-Code ist einem Realm zugeordnet, wenn es geladen wird, der gleich bleibt, selbst wenn er von einem anderen Realm aus aufgerufen wird. Ein Realm besteht aus den folgenden Informationen:
Im Web korrespondieren das Realm und das globale Objekt 1-zu-1. Das globale Objekt ist entweder ein Window, ein WorkerGlobalScope oder ein WorkletGlobalScope. Zum Beispiel führt jedes iframe in einem anderen Realm aus, obwohl es sich im selben Agenten wie das übergeordnete Fenster befinden kann.
Realms werden normalerweise erwähnt, wenn es um die Identitäten globaler Objekte geht. Zum Beispiel benötigen wir Methoden wie Array.isArray() oder Error.isError(), weil ein Array, das in einem anderen Realm konstruiert wurde, ein anderes Prototypobjekt als das Array.prototype-Objekt im derzeitigen Realm hat, sodass instanceof Array fälschlicherweise false zurückgeben würde.
Wir betrachten zuerst die synchrone Code-Ausführung. Jeder Job startet mit dem Aufruf seines zugehörigen Callbacks. Code innerhalb dieses Callback kann Variablen erstellen, Funktionen aufrufen oder enden. Jede Funktion muss ihre eigenen Variablendeklarationen und den Rücksprungpunkt speichern. Um dies zu handhaben, benötigt der Agent einen Stapel, um die Ausführungskontexte zu verfolgen. Ein Ausführungskontext, auch allgemein als Stapelrahmen bekannt, ist die kleinste Ausführungseinheit. Er verfolgt die folgenden Informationen:
Stellen Sie sich ein Programm vor, das aus einem einzelnen Job besteht, der durch den folgenden Code definiert ist:
Wenn ein Frame entfernt wird, ist er nicht unbedingt für immer verschwunden, denn manchmal müssen wir zu ihm zurückkehren. Betrachten Sie zum Beispiel eine Generatorfunktion:
In diesem Fall erstellt der Aufruf von gen() zuerst einen Ausführungskontext, der angehalten wird—kein Code innerhalb von gen wird bis jetzt ausgeführt. Der Generator g speichert diesen Ausführungskontext intern. Der aktuell laufende Ausführungskontext bleibt der Einstiegspunkt. Wenn g.next() aufgerufen wird, wird der Ausführungskontext für gen auf den Stapel geschoben und der Code innerhalb von gen wird bis zum yield-Ausdruck ausgeführt. Dann wird der Ausführungskontext des Generators angehalten und aus dem Stapel entfernt, was die Kontrolle zurück an den Einstiegspunkt gibt. Wenn g.next() erneut aufgerufen wird, wird der Ausführungskontext des Generators wieder auf den Stapel geschoben und der Code innerhalb von gen wird dort fortgesetzt, wo er aufgehört hat.
Ein Mechanismus, der in der Spezifikation definiert ist, ist der proper tail call (PTC). Ein Funktionsaufruf ist ein Tail Call, wenn der Aufrufer nach dem Aufruf nichts anderes tut, als den Wert zurückzugeben:
In diesem Fall ist der Aufruf von g ein Tail Call. Wenn ein Funktionsaufruf in Tail-Position ist, wird die Engine angewiesen, den aktuellen Ausführungskontext zu verwerfen und ihn durch den Kontext des Tail Calls zu ersetzen, anstelle eines neuen Frames für den g()-Aufruf. Dies bedeutet, dass Tail-Rekursion nicht den Stapelgrößenbeschränkungen unterliegt:
In der Praxis verursacht das Verwerfen des aktuellen Frames Debugging-Probleme, denn wenn g() einen Fehler wirft, ist f nicht mehr im Stapel und erscheint nicht im Stack-Trace. Derzeit implementiert nur Safari (JavaScriptCore) PTC, und sie haben einige spezielle Infrastruktur erfunden, um dieses Debugging-Problem zu lösen.
Ein weiteres interessantes Phänomen im Zusammenhang mit Variablen-Bereich und Funktionsaufrufen sind Closures. Wann immer eine Funktion erstellt wird, merkt sie sich auch die Variablenbindungen des aktuell laufenden Ausführungskontexts intern. Dann können diese Variablenbindungen den Ausführungskontext überdauern.
Ein Agent ist ein Thread, was bedeutet, dass der Interpreter nur eine Anweisung auf einmal verarbeiten kann. Wenn der Code vollständig synchron ist, ist dies in Ordnung, da wir immer Fortschritte machen können. Aber wenn der Code asynchrone Aktionen durchführen muss, dann können wir nicht weiter machen, es sei denn, diese Aktion ist abgeschlossen. Es wäre jedoch für die Benutzererfahrung nachteilig, wenn das das gesamte Programm anhalten würde—die Natur von JavaScript als Web-Skriptsprache erfordert, dass es niemals blockiert. Daher wird der Code, der die Fertigstellung dieser asynchronen Aktion verarbeitet, als Callback definiert. Dieses Callback definiert einen Job, der in eine Aufgabenwarteschlange—oder in HTML-Begriffen, eine Ereignisschleife—nach Abschluss der Aktion gesetzt wird.
Jedes Mal zieht der Agent einen Job aus der Warteschlange und führt ihn aus. Wenn der Job ausgeführt wird, kann er weitere Jobs erstellen, die am Ende der Warteschlange hinzugefügt werden. Jobs können auch durch den Abschluss asynchroner Plattformmechanismen wie Timer, I/O und Ereignisse hinzugefügt werden. Ein Job wird als abgeschlossen betrachtet, wenn der Stapel leer ist; dann wird der nächste Job aus der Warteschlange gezogen. Die Jobs können möglicherweise nicht mit gleichmäßiger Priorität ausgeführt werden—zum Beispiel teilen HTML-Ereignisschleifen Jobs in zwei Kategorien auf: Aufgaben und Mikroaufgaben. Mikroaufgaben haben eine höhere Priorität und die Mikroaufgaben-Warteschlange wird zuerst geleert, bevor die Aufgaben-Warteschlange aufgerufen wird. Für weitere Informationen lesen Sie den HTML-Mikroaufgaben-Leitfaden. Wenn die Arbeitswarteschlange leer ist, wartet der Agent, bis weitere Jobs hinzugefügt werden.
Jeder Job wird vollständig verarbeitet, bevor ein anderer Job verarbeitet wird. Dies bietet einige nette Eigenschaften bei der Argumentation über Ihr Programm, einschließlich der Tatsache, dass, wenn eine Funktion ausgeführt wird, sie nicht unterbrochen werden kann und vollständig ausgeführt wird, bevor ein anderer Code ausgeführt wird (und Daten, mit denen die Funktion arbeitet, geändert werden können). Dies unterscheidet sich von C, wo, wenn eine Funktion in einem Thread läuft, sie an jedem Punkt vom Laufzeitsystem angehalten werden kann, um einen anderen Code in einem anderen Thread auszuführen.
Zum Beispiel, betrachten Sie dieses Beispiel:
In diesem Beispiel erstellen wir ein bereits aufgelöstes Promise, was bedeutet, dass jeder angefügte Callback sofort als Jobs eingeplant wird. Die beiden Callbacks scheinen eine Race-Bedingung zu verursachen, aber tatsächlich ist die Ausgabe vollständig vorhersehbar: 1 und 2 werden in Reihenfolge protokolliert. Dies liegt daran, dass jeder Job vollständig ausgeführt wird, bevor der nächste gestartet wird, sodass die Gesamtreihenfolge immer i += 1; console.log(i); i += 1; console.log(i); und niemals i += 1; i += 1; console.log(i); console.log(i); ist.
Ein Nachteil dieses Modells ist, dass, wenn ein Job zu lange braucht, um abgeschlossen zu werden, die Webanwendung nicht in der Lage ist, Benutzerinteraktionen wie Klicken oder Scrollen zu verarbeiten. Der Browser mildert dies mit dem Dialog "Ein Skript läuft zu lange". Eine gute Praxis ist, die Bearbeitung von Jobs kurz zu halten und, wenn möglich, einen Job in mehrere Jobs zu unterteilen.
Ein weiteres wichtiges Versprechen des Ereignisschleifenmodells ist, dass die JavaScript-Ausführung niemals blockiert. Die Behandlung von Eingabe/Ausgabe wird normalerweise über Ereignisse und Callbacks durchgeführt, sodass die Anwendung, während sie auf eine Antwort einer IndexedDB-Abfrage oder einen fetch()-Anfrage wartet, weiterhin andere Dinge wie Benutzereingaben verarbeiten kann. Der Code, der nach der Fertigstellung einer asynchronen Aktion ausgeführt wird, wird immer als Callback-Funktion bereitgestellt (zum Beispiel, der Promise then()-Handler, die Callback-Funktion in setTimeout() oder der Ereignis-Handler), was einen Job definiert, der in die Aufgabenschlange eingefügt wird, sobald die Aktion beendet ist.
Natürlich erfordert das Versprechen "niemals blockieren", dass die Plattform-API inhärent asynchron ist, aber einige veraltete Ausnahmen existieren, wie alert() oder synchrones XHR. Es wird als gute Praxis angesehen, diese zu vermeiden, um die Reaktionsfähigkeit der Anwendung sicherzustellen.
Mehrere Agenten können über die gemeinsame Nutzung von Speicher kommunizieren und bilden dabei einen Agenten-Cluster. Agenten befinden sich im selben Cluster, wenn und nur wenn sie Speicher teilen können. Es gibt keinen eingebauten Mechanismus, damit zwei Agenten-Cluster Informationen austauschen können, sodass sie als vollständig isolierte Ausführungsmodelle angesehen werden können.
Beim Erstellen eines Agenten (wie zum Beispiel durch das Starten eines Workers), gibt es einige Kriterien dafür, ob er sich im selben Cluster wie der aktuelle Agent befindet oder ob ein neuer Cluster erstellt wird. Zum Beispiel gehören die folgenden Paare von globalen Objekten jeweils zum selben Agenten-Cluster und können daher Speicher miteinander teilen:
Die folgenden Paare von globalen Objekten gehören nicht zum selben Agenten-Cluster und können daher keinen Speicher teilen:
Für den genauen Algorithmus siehe die HTML-Spezifikation.
Wie bereits erwähnt, kommunizieren Agenten über die gemeinsame Nutzung von Speicher. Im Web wird Speicher über die Methode postMessage() geteilt. Der Leitfaden zur Verwendung von Web-Workern bietet einen Überblick darüber. Typischerweise werden Daten nur durch Wert übergeben (durch strukturierte Duplizierung) und beinhalten daher keine Probleme mit der Nebenläufigkeit. Um Speicher zu teilen, muss ein SharedArrayBuffer-Objekt gepostet werden, das von mehreren Agenten gleichzeitig verwendet werden kann. Sobald zwei Agenten Zugriff auf denselben Speicher über einen SharedArrayBuffer teilen, können sie die Ausführungen über das Atomics-Objekt synchronisieren.
Es gibt zwei Möglichkeiten, auf den gemeinsamen Speicher zuzugreifen: über normale Speicherzugriffe (die nicht atomar sind) und über atomare Speicherzugriffe. Letztere sind sequentiell konsistent (das bedeutet, dass es eine strenge Gesamtordnung von Ereignissen gibt, auf die sich alle Agenten im Cluster einigen), während die erstere ungeordnet ist (das bedeutet, dass es keine Ordnung gibt); JavaScript bietet keine Operationen mit anderen Ordnungsversprechen.
Die Spezifikation gibt die folgenden Richtlinien für Programmierer, die mit gemeinsamem Speicher arbeiten:
Wir empfehlen, Programme rennbedingungsfrei zu halten, d.h. sicherzustellen, dass es unmöglich ist, dass nicht-atomare Vorgänge gleichzeitig auf derselben Speicherstelle stattfinden. Rennbedingungsfreie Programme haben Verflechtungssemantiken, bei denen jeder Schritt in der Evaluierungssemantik jedes Agenten untereinander verflochten ist. Für rennbedingungsfreie Programme ist es nicht notwendig, die Details des Speicher-Modells zu verstehen. Die Details werden wahrscheinlich keine Intuition vermitteln, die dabei hilft, ECMAScript besser zu schreiben.
Allgemeiner gesagt, auch wenn ein Programm nicht rennbedingungsfrei ist, kann es vorhersehbares Verhalten haben, solange atomare Vorgänge nicht in Races involviert sind und die Vorgänge, die in Races involviert sind, alle dieselbe Zugriffsgröße haben. Der einfachste Weg, um sicherzustellen, dass Atomics nicht in Rennen verwickelt sind, besteht darin, sicherzustellen, dass unterschiedliche Speicherzellen von atomaren und nicht-atomaren Vorgängen verwendet werden und dass atomare Zugriffe unterschiedlicher Größen nicht gleichzeitig auf dieselben Zellen zugreifen. Effektiv sollte das Programm versuchen, den gemeinsam genutzten Speicher so stark wie möglich typisiert zu behandeln. Man kann sich immer noch nicht auf die Ordnung und das Timing von nicht-atomaren Zugriffen, die in Rennen stehen, verlassen, aber wenn Speicher stark typisiert behandelt wird, werden die rennbedingten Zugriffe nicht "zerreißen" (Teile ihrer Werte werden nicht durchgemischt).
Wenn mehrere Agenten zusammenarbeiten, gilt das Versprechen des niemals blockierenden nicht immer. Ein Agent kann blockiert oder pausiert werden, während er darauf wartet, dass ein anderer Agent eine Aktion durchführt. Dies unterscheidet sich von einem Warten auf ein Versprechen im selben Agenten, da es den gesamten Agenten stoppt und keinen anderen Code in der Zwischenzeit ausführen lässt—in anderen Worten, es kann keine Fortschritte machen.
Um Deadlocks zu verhindern, gibt es starke Einschränkungen, wann und welche Agenten blockiert werden können.
Der Agenten-Cluster sorgt für ein gewisses Maß an Integrität über die Aktivität seiner Agenten im Falle externer Pausen oder Beendigungen:
| ECMAScript® 2027 Language Specification |
| ECMAScript® 2027 Language Specification |
| HTML |
Der Bauplan für ein besseres Internet.
Besuche die gemeinnützige Muttergesellschaft der Mozilla Corporation, die Mozilla Foundation.
Teile dieses Inhalts sind ©1998–2026 von einzelnen mozilla.org-Mitwirkenden. Inhalte sind verfügbar unter einer Creative-Commons-Lizenz.