Memory Life Cycle (Speicherlebenszyklus)

Der Speicherlebenszyklus bezieht sich darauf, wie eine Programmiersprache mit dem Speicher arbeitet. Unabhängig von der Sprache ist der Speicherlebenszyklus fast immer gleich. Er besteht aus drei Schritten.

1.Die Speicherzuweisung ("Memory Creation")


Der erste Schritt ist die Speicherzuweisung ("Memory Creation"). Wenn wir eine Variable zuweisen, eine Funktion oder ein Objekt erstellen, muss dafür eine gewisse Menge an Speicher reserviert werden. In der ersten Phase, wenn die JS-Engine unser Skript erhält, richtet sie als erstes Speicher für die Daten in unserem Code ein. Der Code wird zunächst Zeile für Zeile durchlaufen und Funktionen und Variablen werden im Memoryspeicher platziert. Zu diesem Zeitpunkt wird noch kein Code ausgeführt, sondern alles für die Ausführung vorbereitet.

Was ist Hoisting?

Unter Hoisting wird eine allgemeine Denkweise verstanden, wie Ausführungskontexte (insbesondere die Erstellungs- und Ausführungsphasen) in JavaScript funktionieren (MDN-Definition). Die Art und Weise, wie Funktionsdeklarationen und Variablen gespeichert werden, ist unterschiedlich: Funktionen werden mit einem Verweis auf die gesamten Funktionen gespeichert. Variablen mit dem Schlüsselwort "var" werden als undefined gespeichert und mit den Schlüsselwörtern "let" oder "const" nicht initialisiert und gespeichert.

2. Die Speichernutzung (Code Execution)


Da Funktionen mit einer Referenz auf den gesamten Funktionscode gespeichert werden, können wir sie sogar vor der Zeile aufrufen, in der wir sie erstellt haben! Da verhindert werden soll, dass versehentlich auf eine undefined-Variable verwiesen wird, wird ein ReferenceError ausgelöst, wenn wir auf nicht initalisierte Variablen zugreifen wollen.

Es gibt da auch noch die sogenannte „Temporal Deadzone“. Sie ist eine zeitlich begrenzte Phase/Zone, in der die Variable keinen Wert hat und die Engine eine Fehlermeldung, anstelle von undefined auswirft. Wenn die Engine dann zu der Zeile kommt, in der wir unsere Variablen deklariert haben, werden die Werte im Speicher mit unseren Werten überschrieben.

Der zweite Schritt ist also die Speichernutzung (Code Execution). Wenn wir mit Daten in unserem Code arbeiten (lesen oder schreiben), verwenden wir Speicher. Das Lesen von Variablen oder das Ändern von Werten verursacht das Lesen und Schreiben in den Speicher. Nach Abschluss der ersten Phase wird der Code tatsächlich ausgeführt.

3. Die Speicherfreigabe


Der dritte Schritt ist die Speicherfreigabe. Wenn wir eine Funktion oder ein Objekt nicht mehr verwenden, kann dieser Speicher wieder freigegeben werden. In dem Moment, in dem er freigegeben ist, kann er wieder verwendet werden. Der Garbage Collector kommt ins Spiel und schafft Platz.


Nun kommen wir zu der Frage, wo diese Variablen, Funktionen und Objekte eigentlich gespeichert werden? Die Antwort lautet: Es kommt darauf an. JavaScript speichert nicht all diese Dinge am selben Ort. Stattdessen verwendet JavaScript zwei Stellen. Diese Orte sind der "Stack" und der "Memory Heap". Welche dieser Orte verwendet werden, hängt davon ab, womit wir gerade arbeiten.


Der "Stack" (Der Stapel)


Der Stack ist ein Ort, an dem JavaScript nur statische Daten speichert. Dazu gehören Werte von primitiven Datentypen, wie:

Die Größe dieser Daten ist festgelegt und JavaScript kennt diese Größe zur Compile-Time (kurz bevor der Code ausgeführt wird). Dies bedeutet auch, dass JavaScript weiß, wie viel Speicher sie zuweisen soll, und ordnet diese Menge zu. Diese Art der Speicherzuweisung wird als "statische Speicherzuweisung" bezeichnet.

Wichtig zu wissen ist, dass es eine Grenze gibt, wie groß diese primitiven Werte sein können. Dies gilt auch für den Stack selbst. Denn auch dieser hat Grenzen. Wie hoch diese Limits sind, hängt vom jeweiligen Browser und der Engine ab.


Abgrenzung zum "Call Stack"
Hinweis: Der "Stack" ist ein Ort, den JavaScript verwendet, um Variablen zu speichern, denen primitive Werte zugewiesen sind. Der sogenannte "Call Stack" ist etwas anderes als der "Stack" und wird hier näher beschrieben.

Der "Memory Heap" (Der Speicherhaufen)


Der zweite Ort, an dem JavaScript Daten speichern kann, ist der "Memory Heap". Diese Art der Speicherzuweisung wird als "dynamische Speicherzuweisung" bezeichnet. Denn dem "Memory Heap", weist JavaScript keine feste Speichermenge zu. Stattdessen weist es Speicher nach Bedarf zu.

Während der Stack ein Ort ist, an dem JavaScript statische Daten speichert, ist der Memory Heap ein Ort, an dem JavaScript Objekte (Reference Types) speichert. Da in JavaScript sogut wie alles ein Objekt ist (Wer kennt das Sprichwort nicht?), trifft es auf vieles zu (Funktionen, Arrays...).

Stack, Heap und References


Wir wissen nun: Wenn wir eine Variable erstellen und ihr einen primitiven Wert zuweisen, wird sie im Stack gespeichert. Etwas anderes passiert, wenn wir dasselbe, jedoch mit einem Objekt versuchen. Wenn wir eine Variable deklarieren und ihr ein Objekt zuweisen, passieren zwei Dinge:

  1. Zunächst weist JavaScript für diese Variable Speicher im Stack zu.
  2. Wenn es um das Objekt selbst geht, speichert JavaScript es im Memory Heap. Die im Stack vorhandene Variable zeigt nur auf dieses Objekt im Memory Heap. Diese Variable ist nun eine Referenz, die auf das Objekt zeigt.

Wir können uns eine Referenz als Verknüpfung oder als Alias für vorhandene Dinge vorstellen (denkt an die Symbole und Verknüpfungen auf eurem Desktop). Dieser Verweis ist also nicht das Objekt selbst. Er ist nur ein Link zum "echten" Objekt. Wir können diesen Link verwenden, um auf das Objekt zuzugreifen, auf das er verweist/ mit dem er verlinkt ist. Dadurch können wir dann auch an ihm "herum manipulieren" (z.B. Funktionen anwenden, etwas hinzufügen usw.).

Kopieren von Primitive Types (Values) und Reference Types (Objects)


Das Erstellen von Kopien von Objekten ist in JavaScript nicht so einfach. Wenn wir versuchen, eine Kopie eines in einer Variablen gespeicherten Objekts zu erstellen, indem wir darauf verweisen, wird keine echte Kopie erstellt. Das Objekt selbst wird nicht kopiert. Es wird nur die Referenz auf dieses Objekt kopiert. Dies wird als flat copy bezeichnet.

Wenn wir dann das Originalobjekt ändern, ändert sich auch die Kopie. Dies liegt daran, dass es immer noch nur ein Objekt gibt . Es gibt jedoch zwei Verweise (Aliasname oder Link) auf dieses eine Objekt. Wenn wir eine dieser Referenzen verwenden, um das Objekt zu ändern, verweist die andere Referenz immer noch auf dasselbe Objekt, das wir gerade geändert haben.

Das passiert nicht, wenn wir stattdessen den primitiven Wert kopieren. Wenn wir versuchen, den Grundwert zu kopieren, und wir dann das Original ändern, bleibt die Kopie unverändert. Der Grund: Die Referenzen fehlen. Wir erstellen also eine echte Kopie und arbeiten dann direkt mit dieser Kopie. Dies echte Kopie wird als deep copy bezeichnet.

Wenn man das Objekt nicht erneut "from scratch" erstellen möchte, hat man folgende Möglichkeiten: - Object.assign()
- Kombination aus JSON.parse() and JSON.stringify() (siehe hier) - siehe auch Spread-Operator