"Dann schauen wir dem Ding mal unter die Haube..."

Natürlich ist es spannend, sich einmal genauer anzuschauen, was bei der Ausführung eines Codes eigentlich passiert. Die JS-Engine funktioniert wie folgt: In unserem HTML-Dokument ist ein JS-Script eingebettet. Sobald der HTML-Parser auf diesen <script>-Tag mit einer Quelle trifft, geht es los. Der Code aus der Quelle wird entweder aus dem Netzwerk, Cache oder einem installierten Service Worker geladen. Die Antwort ist das angeforderte Script als ein "Stream of bytes"(Byte-Stream), um den sich im Folgenden der Byte-Stream-Decoder kümmert!

Job of JS-Engine in one sentence

I take your JS, parse it, turn that into an Abstract Syntax Tree, generate Bytecode, get feedback (for speculative optimizations), optimize and compile it.

Der Byte-Stream-Decoder


Der Byte-Stream-Decoder decodiert (wie der Name es schon vermuten lässt) den Byte-Stream, während dieser heruntergeladen wird.
Der Byte-Stream-Decoder erstellt sogenannte Token aus dem decodierten Bytestream. Das reservierte Schlüsselwort „function“ entsteht(siehe Beispiel), ein Token wird erstellt und an den Parser und den Pre-Parser gesendet. Die Engine verwendet nämlich zwei Parser: den Pre-Parser und den Parser. Denn, um die Ladezeit einer Website zu verkürzen, versucht die Engine, das "Parsen" des nicht sofort benötigen Codes zu vermeiden. Der Pre-Parser verarbeitet Code, der später verwendet werden kann, während der Parser den Code verarbeitet, der sofort benötigt wird.

Beispiel: decodierter Bytestream

Der Parser erstellt Knoten basierend auf den Token, die er vom Byte-Stream-Decoder empfängt. Mit diesen Knoten erstellt er einen „abstract syntax tree“ (AST).

Der Interpreter


Als Nächstes ist es Zeit für den Interpreter (Dolmetscher).
Der Interpreter, durchläuft den AST und generiert Bytecode basierend auf den Informationen, die der AST enthält. Sobald der Bytecode vollständig generiert wurde, wird der AST gelöscht, wodurch Speicherplatz freigegeben wird. Der Bytecode versucht sich mithilfe des "Optimizing Compilers" immer weiter zu verbessern/ zu verschnellern. Dazu werden, während der Bytecode ausgeführt wird, Informationen generiert. Es wird bspw. erkannt, ob ein bestimmtes Verhalten häufig auftritt und welche Datentypen verwendet werden. Wenn bspw. eine Funktion schon viele Male aufgerufen wurde, wird sie wie folgt optimiert.

Der "optimizing Compiler"


  1. Dieser Compiler nimmt den Bytecode und das „Type-Feedback" und generiert daraus hochoptimierten Maschinencode.
  2. Der Bytecode wird zusammen mit dem generierten „Type-Feedback“ an den „Optimizing Compiler" gesendet.
  3. JavaScript ist sehr dynamisch, was bedeutet, dass sich die Datentypen ständig ändern können.
  4. Es wäre extrem langsam, wenn die JavaScript-Engine jedes Mal überprüfen müsste, welchen Datentyp ein bestimmter Wert hat.
  5. Um die Zeit für die Interpretation des Codes zu verkürzen, behandelt der „Optimizing Compiler" nur die Fälle, die die Engine zuvor beim Ausführen des Bytecodes gesehen hat.
  6. Wenn wir einen bestimmten Codeabschnitt wiederholt verwendet haben, der immer wieder den gleichen Datentyp zurückgegeben hat, kann der optimierte Maschinencode einfach wiederverwendet werden, um die Dinge zu beschleunigen.
  7. Da JavaScript jedoch dynamisch typisiert ist, kann es vorkommen, dass derselbe Code plötzlich einen anderen Datentyp zurückgibt. In diesem Fall wird, anstelle des optimierten Maschinencodes, wieder der ursprüngliche Bytecode verwendet.