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.
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.
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.
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.
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.
Der Stack ist ein Ort, an dem JavaScript nur statische Daten speichert. Dazu gehören Werte von primitiven Datentypen, wie:
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 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...).
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:
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.).
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