Primitives vs. Reference Types in JavaScript: Ein Blick unter die Haube

Max Schneider
Max Schneider ·
Primitives vs. Reference

Primitives vs. Reference Types in JavaScript: Ein Blick unter die Haube

In der Welt von JavaScript ist das Verstehen der Unterschiede zwischen primitiven Datentypen und Referenztypen entscheidend. Diese Konzepte beeinflussen, wie Daten gespeichert, kopiert und manipuliert werden. Doch was steckt wirklich dahinter? In diesem Blogbeitrag tauchen wir tief in die Funktionsweise dieser beiden grundlegenden Konzepte ein.


Primitive Datentypen: Wertkopie

Was sind Primitive Datentypen?

Primitive Datentypen in JavaScript umfassen:

  • string
  • number
  • boolean
  • undefined
  • null
  • symbol
  • bigint

Diese Typen sind immutable, was bedeutet, dass ihr Wert nach der Initialisierung nicht verändert werden kann. Stattdessen wird bei Änderungen eine neue Kopie erstellt.

Speicherstruktur bei Primitives

Primitives werden direkt im Call Stack gespeichert, da sie klein und einfach sind. Diese Speicherstrategie macht sie besonders effizient.

Wie funktionieren Wertkopien?

Wenn eine Variable mit einem primitiven Wert einer anderen Variable zugewiesen wird, wird eine unabhängige Kopie des Werts erstellt.

Beispiel:

let a = 42; // Der Wert 42 wird in 'a' gespeichert
let b = a; // Eine Kopie von 42 wird in 'b' gespeichert
 
b = 99; // 'b' wird geändert, 'a' bleibt unverändert
console.log(a); // Ausgabe: 42
console.log(b); // Ausgabe: 99

Im Speicher:

Variable   Speicherort   Wert
a          1001          42
b          1002          42 (Kopie von a)

Warum sind Primitive immutable?

Diese Unveränderlichkeit sorgt für:

  1. Sicherheit: Werte sind unabhängig voneinander.
  2. Effizienz: Kleinere und einfachere Speicherverwaltung.

Referenztypen: Zeigerkopie

Was sind Referenztypen?

Referenztypen umfassen:

  • Object
  • Array
  • Function

Referenztypen sind mutable, das heißt, sie können nach der Initialisierung geändert werden. Statt den Wert selbst zu speichern, speichert die Variable lediglich einen Zeiger (Pointer) auf den Speicherort des Objekts im Heap.

Speicherstruktur bei Referenztypen

Der Heap ist ein separater Speicherbereich für komplexe und dynamische Daten. Der Call Stack enthält lediglich die Referenz auf die Adresse im Heap.

Wie funktionieren Zeigerkopien?

Wenn eine Referenzvariable einer anderen zugewiesen wird, wird nur der Zeiger kopiert, nicht das Objekt selbst.

Beispiel:

let obj1 = { id: 1, name: "Max" };
let obj2 = obj1;
 
obj2.id = 2;
 
console.log(obj1.id); // Ausgabe: 2
console.log(obj2.id); // Ausgabe: 2

Im Speicher:

Variable   Speicherort   Wert
obj1       2001          Zeiger -> [ Heap 3001: { id: 2, name: "Max" } ]
obj2       2002          Zeiger -> [ Heap 3001 ]

Beide Variablen zeigen auf denselben Speicherort im Heap, daher wirken sich Änderungen überall aus.


Vergleich der Speicherverwaltung in JavaScript und C++

Automatische vs. manuelle Speicherverwaltung

  • JavaScript: Die Speicherverwaltung erfolgt automatisch durch Garbage Collection.
  • C++: Entwickler müssen Speicher explizit anfordern und freigeben.

Beispiel eines Speicherlecks in C++:

int* ptr = new int(42);
ptr = nullptr; // Speicher wird nicht freigegeben, Leck entsteht

Speicherfreigabe in C++

In C++ wird Speicher manuell mit delete (für new) oder free (für malloc) freigegeben. Moderne Ansätze wie Smart Pointers (z. B. std::unique_ptr) erleichtern jedoch die Verwaltung.

Beispiel für manuelle Speicherfreigabe:

#include <iostream>
using namespace std;
 
int main() {
    int* ptr = new int(42); // Speicher wird im Heap zugewiesen
    cout << *ptr << endl; // Ausgabe: 42
 
    delete ptr; // Speicher wird freigegeben
    ptr = nullptr; // Zeiger wird zur Sicherheit auf nullptr gesetzt
 
    return 0;
}

Automatische Speicherverwaltung in JavaScript

Dank der Garbage Collection in JavaScript müssen Entwickler sich nicht um die Speicherfreigabe kümmern. Der Garbage Collector entfernt ungenutzte Objekte aus dem Heap, wenn sie nicht mehr referenziert werden.


Herausforderungen bei Referenztypen

Shallow Copy vs. Deep Copy

Bei Referenztypen führt das Kopieren oft zu unerwarteten Seiteneffekten. Um ein Objekt vollständig zu kopieren, muss eine Deep Copy erstellt werden.

Beispiel:

let original = { id: 1, nested: { value: 42 } };
let shallowCopy = { ...original }; // Nur die oberste Ebene wird kopiert
let deepCopy = JSON.parse(JSON.stringify(original)); // Vollständige Kopie
 
shallowCopy.nested.value = 99;
console.log(original.nested.value); // Ausgabe: 99 (seiteneffektbehaftet)
 
deepCopy.nested.value = 100;
console.log(original.nested.value); // Ausgabe: 99

Lösung: Utility-Funktionen

Für komplexe Kopiervorgänge stehen in modernen JavaScript-Bibliotheken wie Lodash Hilfsmittel bereit:

Beispiel mit Lodash:

import cloneDeep from "lodash/cloneDeep";
 
let deepCopy = cloneDeep(original);
deepCopy.nested.value = 200;
console.log(original.nested.value); // Ausgabe: 99

Fazit

Das Verständnis der Unterschiede zwischen primitiven und Referenztypen in JavaScript ist essenziell für das Schreiben von robustem und effizientem Code. Primitive Typen sind unveränderlich und einfach zu kopieren, während Referenztypen komplexer sind und zusätzliche Maßnahmen wie Deep Copies erfordern, um ungewollte Seiteneffekte zu vermeiden. Die automatische Speicherverwaltung in JavaScript vereinfacht vieles, erfordert jedoch ein solides Verständnis der zugrunde liegenden Mechanismen, um effektiv programmiert zu werden.

Max

Hallo 👋 Ich bin Max dein freiberuflicher Softwareentwickler und Author des Blogs. Du kannst meine Arbeit auf Social Media verfolgen.

Mehr Blogbeiträge