navigation Navigation


Inhaltsverzeichnis

Symbol


JavaScript‑Symbole (Symbol) sind eindeutige, unveränderliche Primitive, die sich als konfliktfreie Schlüssel und für interne Eigenschaften in Objekten eignen. Sie verhindern Namenskollisionen, bleiben bei normalen Aufzählungen verborgen und bilden die Grundlage für Meta‑Programmierung über well‑known symbols wie Symbol.iterator, Symbol.toStringTag oder Symbol.toPrimitive. Der Deep‑Dive behandelt Erzeugung, Vergleich und Sichtbarkeit von Symbolen, den globalen Registry‑Mechanismus (Symbol.for / Symbol.keyFor), typische Anwendungsfälle in Bibliotheken und APIs sowie Fallstricke und Best Practices für robuste, wartbare Implementierungen.

Inhaltsverzeichnis

    Einführung

    Symbol ist in JavaScript ein primitiver Datentyp, der eingeführt wurde, um eindeutige Kennungen zu erstellen. Jedes Symbol ist einzigartig, selbst wenn es mit derselben Beschreibung erzeugt wird. Es dient in erster Linie dazu, Kollisionen von Eigenschaftsnamen in Objekten zu vermeiden – also Fälle, in denen mehrere Teile eines Programms versehentlich denselben Schlüsselnamen verwenden.

    Was ist ein Wert in JavaScript?

    Zuerst sprechen wir und erinnern uns an ein paar allgemeine Konzepte und Gegebenheiten aus dem Bereich Computer Science!

    Der Speicher

    Bevor wir über Symbole sprechen können, müssen wir kurz ins Gedächtnis holen, wie ein Computer überhaupt Daten speichert. Das ist fundamental wichtig, weil ALLES was JavaScript macht - jede Variable, jedes Objekt, jedes Symbol - letztendlich nur Bytes im Speicher sind.

    Man kann sich den RAM als eine gigantische Straße mit nummerierten Häusern vorstellen. Jedes “Haus” kann genau ein Byte (8 Bits) speichern. Diese “Hausnummern” nennt man Speicheradressen und sie sind in hexadezimaler Notation nummeriert.

    In diesem Schema sieht man 8 aufeinanderfolgende Bytes im Speicher. Die Zahl 0x2A an Adresse 0x0001 entspricht der Dezimalzahl 42. Das ist wichtig zu verstehen. Alles im Computer ist nur eine Zahl - ein Buchstabe, ein Symbol, ein Objekt und alles, was man sonst kennt.

    Die Adresse 0x0001 bedeutet: Gehe zum Byte Nr. 1 im Speicher. Das 0x am Anfang bedeutet nur “das ist eine hexadezimale Notation”. Hexadezimal ist praktisch für die Verwendung, weil ein Byte (8 Bits) genau durch 2 hexadezimale Ziffern dargestellt werden kann.

    0 dezimal = 00 hexadezimal

    255 dezimal = FF hexadezimal


    Der Pointer

    Jetzt wird es etwas interessanter. Ein Pointer (auf Deutsch “Zeiger”) ist einfach eine Speicheradresse. Statt einen Wert direkt zu speichern, speichert man die Adresse wo der tatsächliche Wert liegt. Hier kann wieder die Relation/Analogie zwischen der Adresse zu einem Haus und dem tatsächlichen Haus hilfreich sein.

    Ein kleines JavaScript Beispiel.

    Beispiel
    let x = 42;

    Was passiert hier wirklich im Speicher?

    Adresse 0x1000: [42]

    Variable x speichert 0x1000. Die Variable enthält also nicht die Zahl 42 direkt, sondern sie enthält die Information “der Wert liegt bei Adresse 0x1000”.

    Wenn man also nun x verwenden würde, würde Folgendes passieren:

    1. JavaScript schaut in die Variable x und findet dort eine Adresse 0x1000
    2. JavaScript geht zu Adresse 0x1000
    3. JavaScript liest den Wert dort: 42

    Diesen Prozess nennt man Dereferenzierung. Man folgt dem Pointer zu seinem Ziel.

    Warum ist das wichtig für Symbole? Weil Symbole intern als Pointer funktionieren. Wenn man zwei Symbole vergleicht, vergleicht JavaScript nicht deren Inhalt, sondern deren Adressen. Das macht den Vergleich extrem schnell.


    Tagged Values

    Jetzt eine wichtige Frage: Woher weiß JavaScript, ob ein Wert im Speicher eine Zahl, ein String, ein Symbol oder ein Objekt ist?

    Das Problem ist nämlich beim Speichern der Werte folgendes. Ein Wert bietet so gut wie nie eine eindeutige Interpretationsmöglichkeit, also auch eine Möglichkeit der Herleitung des Typs.

    Wenn man nur 42 im Speichert sieht, könnte es sein:

    • Die Zahl 42
    • Der ASCII-Code für das Zeichen '*'
    • Ein Teil einer Speicheradresse
    • Etwas ganz anderes

    JavaScript muss also irgendwie den Typ des Wertes mit speichern. Die Lösung heißt Tagged Pointers (getaggte oder gekennzeichnete Zeiger).

    Die Idee ist interessant und einfach: JavaScript nutzt die “unteren Bits” eines 64-Bit-Werts als “Typ-Marker” (Tag). Die restlichen Bits enthalten den eigentlichen Wert oder einen Pointer.

    64-Bit Wert im Speicher

    Beispiel Tags
    0000 = Small Integer (SMI)
    0001 = Pointer zu Object
    0010 = Pointer zu String
    0011 = Pointer zu Symbol
    ... etc

    Das bedeutet: Die letzten 4 Bits eines Werts sagen in JavaScript, was für ein Typ das ist. Die restlichen 60 Bits enthalten die eigentlichen Daten.

    Veranschaulichen wir das an einem kleinen Beispiel.

    let num = 42;
    let sym = Symbol("test");

    So sehen die Werte wirklich im Speicher aus.

    Variable: num



    Bei der Zahl 42 ist es einfach. Die unteren 4 Bits sind 0000, das Tag für “Small Integer” und die oberen Bits enthalten die binäre Darstellung von 42 (101010).

    Variable: sym



    Bei einem Symbol ist das Tag 0011 (Symbol-Typ) und die oberen Bits enthalten einen Pointer - in diesem Fall 0x5000. Das ist die Adresse im Speicher, wo das eigentliche Symbol-Objekt liegt.

    Symbol in RAM

    Symbol-Objekt Struktur

    Schauen wir uns an, wie man ein Symbol erstellt.

    Symbol erstellen
    let sym = Symbol("mySymbol");

    JavaScript muss jetzt ein Symbol-Objekt im Speicher anlegen. Aber was ist ein Symbol-Objekt? Das ist eine Daatenstruktur - eine bestimmte Anordnung von Bytes, die zusammen ein Symbol repräsentieren.

    Das passiert intern:

    1. JavaScript erstellt einen neuen, einzigartigen “Symbol-Wert”
    2. Dieser bekommt eine EINZIGARTIGE ID (bspw. 0x5000)
    3. Die Beschreibung mySymbol wird separat gespeichert
    Variable sym zeigt auf ==> 0x5000
    Adresse 0x5000: [Typ-Tag: Symbol][Eindeutige-ID: 12345][Beschreibung: "mySymbol"]

    Hier ist folgender Abschnitt der wichtigste: [Eindeutige-ID: 12345].

    Im Speicher sieht das Symbol-Objekt ca. so aus.

    +---------+----------------+--------+----------------------------+
    | Offset  | Feld           | Wert   | Beschreibung               |
    +---------+----------------+--------+----------------------------+
    | +0      | Type Tag       | 0x07   | Symbol-Typ                 |
    | +4      | Hash           | 0x8A3F | Für Hash-Tables            |
    | +8      | Symbol ID      | 23871  | Eindeutige ID              |
    | +16     | Flags          | 0x00   | z. B. global flag          |
    | +20     | Description*   | 0x6000 | Pointer auf String         |
    +---------+----------------+--------+----------------------------+
    
                                    |
                                    v
                            Adresse 0x6000:
                            +------------------+
                            | "meinSymbol"     |
                            +------------------+

    Gehen wir die einzelnen Felder drüber.

    Offset +0 (Type Tag): Das ist ein interner Marker, der der Engine sagt “dies ist ein Symbol”. Jeder Object-Typ hat einen anderen Tag. Das ist zusätzlich zum Pointer-Tag und wird verwendet, wenn man dem Pointer gefolgt ist.

    Offset +4 (Hash): Ein vorberechneter Hash-Wert für dieses Symbol. Wenn das Symbol als Property-Key verwendet wird, kann JavaScript diesen Hash sofort verwenden, ohne ihn neu berechnen zu müssen. Das ist eine Performance-Optimierung.

    Offset +8 (Symbol ID): Das ist das Herzstück. Diese Zahl ist das, was das Symbol wirklich einzigartig macht! Sie wird nie zweimal vergeben. In diesem Beispiel ist es die 23871.

    Offset +16 (Flags): Zusätzliche Informationen. Zum Beispiel: Ist dies ein globales Symbol (von Symbol.for())? Ist es ein Well-Known Symbol (wie Symbol.iterator)? Diese Informationen werden als einzelne Bits gespeichert.

    Offset +20 (Description): Ein Pointer zur Beschreibung. Wichtig: Die Beschreibung ist optional und hat keinen Einfluss auf die Identität des Symbols.

    Es ist also die Symbol ID, die das Symbol ausmacht. Nicht die Beschreibung! Zwei Symbole mit der gleichen Beschreibung haben verschiedene IDs und verschieden.

    Wie wird diese ID generiert? Werfen wir einen Blick auf eine vereinfachte Version.

    Vereinfachte Version
    class Symbol {
        static uint64_t next_id = 0; // Globaler Counter
    
        uint64_t id;
        String* description;
    
        Symbol(String* desc) {
            this->id = ++next_id; // Jedes Symbol bekommt eine neue ID
            this->description = desc;
        }
    }

    Somit ist next_id eine statische Variable, was bedeutet, dass zur Klasse gehört, nicht zu einer einzelnen Instanz. Es gibt nur EINE next_id für ALLE Symbole. Jedes Mal, wenn ein neues Symbol erstellt wird, wird diese Zahl erhöht.

    Das Schlüsselwort static bedeutet: Dieser Wert wird geteil von allem Symbol-Instanzen. Selbst wenn man Millionen Symbole erstellt, gibt es nur EINEN Counter.


    Zwei Symbole vergleichen

    Jetzt verstehen wir, wie Symbole gespeichert werden. Aber was passiert, wenn man sie vergleicht?

    Symbole vergleichen
    let symOne = Symbol("test");
    let symTwo = Symbol("test");
    
    console.log(symOne === symTwo);
    false

    Dieser Code gibt false zurück. Warum? Beide haben die gleiche Beschreibung. Wir wissen aber, dass nicht die Beschreibung ein Symbol ausmacht! Schauen wir uns also an, was im Computer passiert.

    Was macht die CPU?

    Der Prozessor führt die eigentlichen Berechnungen aus. JavaScript Code wird letztendlich zu CPU-Befehlen übersetzt. Hier ist eine vereinfachte Version in Assembly-Sprache (Pseudo-Code).

    Assembly Pseudo-Code
    ; Pseudo-Code
    
    LOAD R1, [symOne]       ; Lade symOne in Register R1
                            ; R1 = 0x5000 | 0x3 (Pointer + Tag)
    
    LOAD R2, [symTwo]       ; Lade symTwo in Register R2
                            ; R2 = 0x5100 | 0x3 (anderer Pointer + Tag)
    
    CMP R1, R2              ; Vergleiche die Werte
                            ; 0x50003 === 0x51003 ?
                            ; NEIN
    
    JNE not_equal           ; Springe zu not_equal

    Der Vergleich ist ein einziger Maschinenbefehl. JavaScript vergleicht einfach die beiden Pointer-Werte. Weil symOne auf Adresse 0x5000 zeigt und symTwo auf 0x5100, sind sie unterschiedlich.

    Symbole als Property-Keys

    Warum Symbole als Objekt-Schlüssel?

    Bevor wir weiter in die technischen Details eintauchen, müssen wir verstehen, warum Symbole überhaupt als Property-Keys in Objekten verwendet werden können und sollten.

    Die Geschichte beginnt mit einem fundamentalen Problem in JavaScript: Namespace-Kollisionen.

    In JavaScript sind Objekte die zentrale Datenstruktur. Alles dreht sich um Objekte. Properties werden String-Keys angesprochen. Das war viele Jahre der einzige Weg, Properties zu benennen. Aber genau hier liegt das Problem. Was passiert, wenn zwei verschiedene Teile eines Programms - vielleicht zwei verschiedene Bibliotheken - dieselben Property-Namen verwenden wollen?

    Das konkrete Problem

    Stellen wir uns vor, wir verwenden eine Bibliothek für Datenvalidierung und eine andere für Datenserialisierung. Beide Bibliotheken wollen an Objekten eine metadata Property hinzufügen.

    Beispiel-Schema
    // Validierungs-Bibliothek
    function validateUser(user) {
        user.metadata = {
            validated: true,
            timestamp: Date.now()
        };
    }
    
    // Serialisierungs-Bibliothek
    function serializeUser(user) {
        user.metadata = {
            format: "json",
            version: 2
        };
    }
    
    const user = { name: "John" };
    validateUser(user);
    serializeUser(user);
    
    // An diesem Punkt ist "metadata" überschrieben
    console.log(user.metadata);
    { format: 'json', version: 2 }

    Die Validierungs-Informationen sind verloren. Die zweite Bibliothek hat die erste überschrieben, weil beide denselben Strin-Key metadata verwendet haben. Das ist eine Namespace-Kollision.

    Natürlich könnte man versuchen, eindeutige Präfixe zu verwenden.

    user.__validation_lib_metadata = { ... };
    user.__serialization_lib_metadata = { ... };

    Aber das ist umständlich, fehleranfällig und letztendlich kann man nie sicher sein, dass nicht doch jemand denselben Präfix verwendet. Außerdem verschmutzt es den öffentlichen Namespace des Objekts.

    Die Lösung mit Symbolen

    Symbole lösen dieses Problem fundamental, weil jedes Symbol garantiert einzigartig ist.

    Lösung mit Symbolen
    // Validierungs-Bibliothek
    const VALIDATION_META = Symbol("validation.metadata");
    
    function validateUser(user) {
        user[VALIDATION_META] = {
            validated: true,
            timestamp: Date.now()
        };
    }
    
    // Serialisierungs-Bibliothek
    const SERIALIZATION_META = Symbol("serialization.metadata");
    
    function serializeUser(user) {
        user[SERIALIZATION_META] = {
            format: "json",
            version: 2
        };
    }
    
    const user = { name: "John" };
    validateUser(user);
    serializeUser(user);
    
    // Beide Metadaten existieren nebeneinander
    console.log(user[VALIDATION_META]);
    console.log(user[SERIALIZATION_META]);
    { validated: true, timestamp: 1762149164569 }
    { format: 'json', version: 2 }

    Beide Bibliotheken können ihre eigenen Metadaten speichern, ohne sich gegenseitig zu stören. Obwohl beide Symbole die gleiche Beschreibung haben könnten, sind sie intern völlig unterschiedlich. Es ist unmöglich, dass sie kollidieren.


    Wie werden Symbol-Keys gespeichert?

    Jetzt wird es noch einmal interessant. Wenn wir ein Symbol als Property-Key verwenden, passiert intern etwas Besonderes. JavaScript behandelt Symbol-Properties anders als String-Properties. Das ist keine willkürliche Design-Entscheidung, sondern hat tiefgreifende Gründe.

    Schauen wir uns ein einfaches Beispiel an.

    Beispiel
    const userId = Symbol("id");
    const userName = "name";
    
    const user = {
        [userId]: 123,
        [userName]: "John"
    };

    Was passiert hier wirklich? JavaScript muss irgendwie beide Properties speichern, aber es behandelt sie intern komplett unterschiedlich.

    Die Objekt-Struktur im Detail

    Jedes JavaScript-Objekt ist im Speicher nicht einfach ein flacher Haufen von Key-Value-Paaren. Es ist eine komplexe Datenstruktur mit verschiedenen Bereichen. Man kann sich ein Objekt wie ein Haus mit mehreren Stockwerken vorstellen, wobei jedes Stockwerk für einen bestimmten Zweck gebaut wurde.

    Stellen wir uns vor, unser user Objekt liegt an der Speicheradresse 0x7000. An dieser Adresse beginnt eine Struktur, die verschiedene Pointer (Verweise) auf andere Speicherbereiche enthält.

    Objekt bei Adresse 0x7000:
    +---------------------------------+
    | Shape/Map Pointer    → 0x7100  | <- Beschreibt die "Form" des Objekts
    | Properties Pointer   → 0x7200  | <- String-Properties werden hier gespeichert
    | Elements Pointer     → 0x7300  | <- Array-ähnliche numerische Indices
    | Symbol Table Pointer → 0x7400  | <- Symbol-Properties werden HIER gespeichert
    +---------------------------------+

    Der entscheidende Punkt: Es gibt einen separaten Bereich nur für Symbol-Properties. Die Symbol-Properties werden nicht zusammen mit den String-Properties gespeichert, sondern in ihrer eigenen Tabelle.

    Schauen wir uns an, wie das konkret aussehen könnte.

    String-Properties-Tabelle bei 0x7200

    +--------------+---------------+
    | String Key   | Value         |
    +--------------+---------------+
    | "name"       | Pointer zu    |
    |              | String "John" |
    +--------------+---------------+

    Symbol-Properties-Tabelle bei 0x7400

    +------------------+--------------+
    | Symbol Pointer   | Value        |
    +------------------+--------------+
    | 0x5000 (userId)  | Number 12345 |
    +------------------+--------------+

    Wichtig: In der Symbol-Tabelle wird nicht die Beschreibung gespeichert, als nicht id, sondern der Pointer zum Symbol-Objekt selbst (0x5000). Das Symbol-Objekt hat eine eindeutige ID.

    Warum diese Trennung?

    Diese Architektur-Entscheidung hat mehrere wichtige Gründe:

    Grund 1: Performance bei normalen Operationen

    Die meisten Operationen in JavaScript arbeiten mit String-Properties. Wenn man user.name schreibt oder Object.keys(user) aufruft, interessieren einen normalerweise nur die String-Properties. Durch die Trennung muss die JavaScript-Engine nicht durch Symbol-Properties iterieren, wenn sie String-Properties sucht. Das macht normale Operationen schneller.

    Stellen wir uns vor, wir hätten ein Objekt mit 100 String-Properties und 50 Symbol-Properties. Jetzt machen wir eine Schleife über alle Keys.

    for (let key in user) {
        console.log(key)
    }

    Ohne die Trennung müsste JavaScript durch alle 150 Properties gehen und bei jeder einzelnen prüfen: “Ist das ein String oder ein Symbol”. Mit der Trennung kann JavaScript einfach sagen: “Gehe nur durch die String-Properties”. Die 50 Symbol-Properties werden einfach ignoriert, weil sie in einer anderen Tabelle liegen.


    Grund 2: Sichtbarkeit und Privatsphäre

    Symbol-Properties sollen bei normalen Enumerationen “unsichtbar” sein. Das ist ein Design-Feature, kein Bug. Wenn jemand Object.keys(user) aufruft, soll er nur die öffentlichen, für ihn gedachten Properties sehen - nicht die internen Metadaten, die Bibliotheken dort gespeichert haben.

    Durch die separate Speicherung ist diese Unsichtbarkeit automatisch gegeben. Die Engine muss nicht bei jeder Property prüfen und entscheiden, ob sie sichtbar sein soll oder nicht. Sie schaut einfach nur in die String-Property-Tabelle.

    Hier eine kleine Veranschaulichung.

    Beispiel - Sichtbarkeit
    // Symbol als Property-Name definieren
    const userSalary = Symbol("salary");
    
    // User Objekt erstellen und normale Properties zuweisen
    const user = {};
    user.name = "John";
    user.age = 40;
    
    // Wert über Symbol zuweisen
    user[userSalary] = 40000;
    
    // Sichtbaren Eigenschaften abrufen
    console.log(Object.keys(user));
    
    // Salary abrufen
    console.log(user[userSalary]);
    
    // Symbole anzeigen
    console.log(Object.getOwnPropertySymbols(user));
    [ 'name', 'age' ]
    40000
    [ Symbol(salary) ]

    Wie man hier sehen kann, sind die Symbol-Properties standardmäßig nicht sichtbar, also nicht “enumerable”.


    Grund 3: Optimierungsmöglichkeiten

    JavaScript-Engines können String-Properties und Symbol-Properties unterschiedlich optimieren. String-Properties werden häufig in “Inline-Caches” gespeichert, weil sie oft verwendet werden. Symbol-Properties brauchen diese Optimierung meist nicht, weil sie seltener angesprochen werden. Durch die Trennung kann die Engine für jeden Typ die optimale Strategie wählen.


    Property-Lookup verstehen

    Wenn JavaScript eine Property lesen will, muss es diese zunächst finden. Dieser Prozess heißt Property Lookup (Eigenschaftssuche). Je nachdem, ob es sich um einen String oder ein Symbol handelt, läuft dieser Prozess unterschiedlich ab.

    String-Property Lookup

    console.log(user.name);

    Was passiert hier Schritt für Schritt?

    1. JavaScript erkennt: name ist ein String-Key (weil es Dot-Notation ist)
    2. JavaScript geht zum Objekt an Adresse 0x7000
    3. JavaScript liest den Properties-Pointer (zeigt auf 0x7200)
    4. JavaScript geht zu Adresse 0x7200 (String-Properties-Tabelle)
    5. JavaScript durchsucht die Tabelle nach dem String "name"
    6. JavaScript findet den Eintrag und liest den Value
    7. JavaScript gibt den Value zurück: "John"

    Symbol-Property-Lookup

    console.log(user[userId]);

    Was passiert hier?

    1. JavaScript schaut sich die Variable userId an
    2. JavaScript erkennt den Tag-Bits: Das ist ein Symbol (Tag 0011)
    3. JavaScript extrahiert den Pointer aus userId: 0x5000
    4. JavaScript geht zum Objekt an Adresse 0x7000
    5. JavaScript liest den Symbol-Tabelle-Pointer (zeigt auf 0x7400)
    6. JavaScript geht zu Adresse 0x7400 (Symbol-Properties-Tabelle)
    7. JavaScript durchsucht die Tabelle nach dem Symbol-Pointer 0x5000
    8. JavaScript findet den Eintrag und liest den Value
    9. JavaScript gibt den Value zurück: 12345

    Der entscheidende Unterschied: Bei Strings muss die Tabelle nach einem String-Match durchsucht werden (Zeichen für Zeichen vergleichen). Bei Symbolen wird nach einem Pointer-Match gesucht (einfacher Integer-Vergleich).


    Warum ist Symbol-Loopup schneller?

    Beim String-Vergleich muss JavaScript potenziell jeden Buchstaben vergleichen. Beim Symbol-Vergleich ist es ein einziger numerischer Vergleich.

    Sichtbarkeit von Symbolen

    Das Konzept der Sichtbarkeitsebenen

    Eines der wichtigsten Features von Symbolen ist ihre gesteuerte Sichtbarkeit. Sie sind nicht einfach “unsichtbar” oder “versteckt” - es gibt verschiedene Ebenen der Sichtbarkeit und verschiedene APIs können auf verschiedenen Ebenen zugreifen.

    Das ist ein durchdachtes Design-Feature. Symbole sollten bei normalen, alltäglichen Operationen unsichtbar bleiben, um keine Verwirrung zu stiften und keine Namenskonflikte zu verursachen. Aber sie sollten trotzdem zugänglich sein, wenn man explizit danach sucht, zum Beispiel für Debugging oder Reflection.

    Schauen wir uns ein Beispiel an.

    const publicKey = "name";
    const hiddenKey = Symbol("secret");
    
    const user = {
        [publicKey]: "John",
        [hiddenKey]: "topSecretValue"
    };

    Dieses einfache Objekt hat zwei Properties. Aber je nachdem, wie wir das Objekt betrachten, sehen wir unterschiedlich viele Properties.

    Ebene 1: Normale Enumeration - Symbole sind unsichtbar

    Die erste und wichtigste Ebene ist die normale Enumeration. Das sind alle Standard-Methoden, die die meisten Entwickler täglich verwenden, um über Objekte zu iterieren oder ihre Keys abzurufen.

    for..in Loop
    for (let key in user) {
        console.log(key, "=", user[key]);
    }
    name = John

    Wie man hier sehen kann, wird Symbol komplett übersprungen.

    Die for...in Schleife sieht nur String-Properties. Warum? Weil sie intern nur die String-Properties-Tabelle durchläuft. Die Symbol-Tabelle wird nicht einmal angeschaut. Das ist bewusst so designed.

    Warum ist das wichtig?

    Stellen wir uns vor, wir verwenden eine externe Bibliothek, die interne Metadaten an Objekten speichert.

    Beispiel
    // Die Bibliothek macht intern
    user[Symbol("lib.internal.cache")] = { ... };
    user[Symbol("lib.internal.state")] = { ... };
    
    // Unser Code
    for (let key in user) {
        console.log(key);
    }

    Ohne diese Unsichtbarkeit würden wir plötzlich Properties sehen, die wir nie erstellt haben. Das würde verwirrend sein und könnte unseren Code brechen, wenn man annimmt, dass alle Properties im Loop im Loop von uns stammen.


    Object.keys()

    Object.keys() gibt ein Array mit allen enumerierbaren String-Keys zurück. Symbol-Keys sind nicht dabei. Das ist konsistent mit der for...in Schleife.

    const keys = Object.keys(user);
    console.log(keys);
    [ 'name' ]

    Was bedeutet “enumerierbar”?

    In JavaScript hat jede Property ein internes Flag namens enumerable. Nur Properties, bei denen diesen Flag true ist, erscheinen in for...in und Object.keys(). Aber Symbol-Properties haben ein zusätzliches “Unsichtbarkeits-Feature”: Selbst wenn das enumerable Flag true wäre, würden sie trotzdem nicht erscheinen, weil sie in einer separaten Tabelle liegen.


    Object.values() und Object.entries()

    Diese Methoden verhalten sich genauso.

    console.log(Object.values(user));
    console.log(Object.entries(user));
    [ 'John' ]
    [ [ 'name', 'John' ] ]

    Sie (Methoden) arbeiten auf der String-Properties-Tabelle und ignorieren die Symbol-Tabelle.


    JSON.stringify()

    Besonders interessant ist das Verhalten bei der Serialisierung.

    const json = JSON.stringify(user);
    console.log(json);
    {"name":"John"}

    Symbol-Properties werden bei JSON-Serialisierung vollständig ignoriert. Das hat einen praktischen Grund: JSON als Format kennt keine Symbole. JSON unterstützt nur Strings als Keys. Daher macht es keinen Sinn, Symbole in JSON aufzunehmen - sie können nicht wieder deserialisiert werden.

    Hier eine praktische Implikation.

    Symbole können gut für Metadaten verwendet werden, die nicht serialisiert werden sollen.

    Beispiel
    const metadata = Symbol("metadata");
    
    const article = {
        title: "JavaScript Symbol",
        content: "...",
        [metadata]: {
            lastModified: Date.now(),
            author: "system",
            internalId: 12345
        }
    };
    
    // Beim Speichern in eine Datenbank oder Senden über eine API
    const json = JSON.stringify(article);
    console.log(json);
    {"title":"JavaScript Symbol","content":"..."}

    Das ist oft genau das gewünschte Verhalten. Interne Metadaten sollen nicht nach außen gehen.

    Ebene 2: Gezielte Symbol-Suche

    Machnmal will man aber doch an die Symbol-Properties ran. Vielleicht für Debugging, oder weil man eine Library schreibt, die mit Symbol-Properties arbeitet. Dafür gibt es spezielle APIs.

    Object.getOwnPropertySymbols()

    Diese Methode gibt ein Array mit allen Symbol-Properties eines Objekts zurück.

    Beispiel
    const publicKey = "name";
    const hiddenKey = Symbol("secret");
    
    const user = {
        [publicKey]: "John",
        [hiddenKey]: "topSecretValue"
    };
    
    const symbols = Object.getOwnPropertySymbols(user);
    
    console.log(symbols);
    console.log(symbols[0] === hiddenKey);
    console.log(user[symbols[0]]);
    [ Symbol(secret) ]
    true
    topSecretValue

    Was passiert intern?

    Diese Methode macht etwas, was normale Enumeration nicht macht: Sie schaut in die Symbol-Tabelle. Sie iteriert durch alle Einträge in der Symbol-Tabelle und gibt die Symbol-Keys zurück.

    Die Methode gibt nur die Keys zurück, nicht die Values (Werte). Um an die Values zu kommen, muss man dann obj[symbol] verwenden.

    Wichtige Einschränkung

    getOwnPropertySymbols() gibt nur die Symbol-Properties des Objekts selbst zurück, nicht die von Prototypen.

    Beispiel
    const parent = {
        [Symbol("parentSymbol")]: "Parent value"
    };
    
    const child = Object.create(parent);
    child[Symbol("childSymbol")] = "Child value";
    
    console.log(Object.getOwnPropertySymbols(child));
    [ Symbol(childSymbol) ]

    Das “Own” in getOwnPropertySymbols bedeutet: Nur eigene Properties, nicht geerbte.


    Ebene 3: Vollständige Reflection

    Die mächtigste Ebene ist die vollstädnige Reflection. Hier bekommt man absolut alles: String-Properties und Symbol-Properties, alle zusammen.

    Reflect.ownKeys()

    Diese Methode gibt ein Array zurück, das sowohl String-Keys als auch Symbol-Keys enthält. Sie kombiniert sozusagen Object.keys() und Object.getOwnPropertySymbols().

    Beispiel
    const publicKey = "name";
    const hiddenKey = Symbol("secret");
    
    const user = {
        [publicKey]: "John",
        [hiddenKey]: "topSecretValue"
    };
    
    const allKeys = Reflect.ownKeys(user);
    console.log(allKeys);
    [ 'name', Symbol(secret) ]

    Warum gibt es diese Methode?

    Reflect.ownKeys() ist Teil der Reflection-API, die in ES6 eingeführt wurde. Diese API ist designed für Meta-Programmierung - also Code, der anderen Code untersucht oder manipuliert. Wenn man wirklich alles über ein Objekt wissen will, braucht man Reflect.ownKeys().

    Hier ein weiteres praktisches Beispiel - Deep Clone.

    Beispiel - Deep Clone
    function deepClone(obj) {
        const clone = {};
    
        // Hole ALLE Keys - Strings und Symbols
        const allKeys = Reflect.ownKeys(obj);
    
        for (let key of allKeys) {
            const value = obj[key];
    
            // Klone den Wert (vereinfacht)
            if (typeof value === "object" && value !== null) {
                clone[key] = deepClone(value);
            } else {
                clone[key] = value;
            }
        }
    
        return clone;
    }
    
    const original = {
        name: "Test",
        [Symbol("id")]: 123
    };
    
    const cloned = deepClone(original);
    
    console.log(cloned.name);
    console.log(Object.getOwnPropertySymbols(cloned));
    Test
    [ Symbol(id) ]

    Ohne Reflect.ownKeys() würde unser Klon nur die String-Properties kopieren und die Symbol-Properties auslassen.


    Die Philosophie hinter den Ebenen

    Diese drei Ebenen sind kein Zufall. Sie repräsentieren unterschiedliche “Intentionen”.

    Ebene 1 (normal): “Ich will mit den öffentlichen, dokumentierten Properties arbeiten”.

    • Das ist der Standard-Fall
    • Hier sollen Symbol-Properties nicht stören

    **Ebene 2 (Symbole): “Ich will speziell mit Symbol-Properties arbeiten”

    • Für Libraries, die mit Symbolen als Mechanismus arbeiten
    • Für Debugging von Symbol-bezogenem Code

    **Ebene 3 (Reflection): “Ich will wirklich ALLES über dieses Objekt wissen”

    • Für Meta-Programmierung
    • Für Serialisierungs-Libraries
    • Für Developer-Tools und Debugger

    Symbol.for() und Symbol.keyFor() - Die globale Registry

    Das Problem wiederverwendbarer Symbole

    Wir erinnern uns, dass Symbole einzigartig sind. Das ist normalerweise genau das, was wir wollen. Aber es gibt Situationen, wo diese Einzigartigkeit zum Problem wird.

    Stellen wir uns folgendes Szenario vor. Wir entwickeln eine große Anwendung mit mehreren Modulen. Mehrere Module müssen auf dieselbe “versteckte” Property zugreifen. Wie machen sie das?

    Der native Ansatz

    Nativer Ansatz
    // module-a.js
    export const USER_ID = Symbol("user.id");
    
    // module-b.js
    import { USER_ID } from "./module-a.js";
    
    // module-c.js
    import { USER_ID } from "./module-c.js";

    Das funktioniert, aber es schafft eine Abhängigkeit. Jedes Modul, das dieses Symbol braucht, muss es von module-a importieren. Das kann problematisch werden:

    1. Was, wenn die Module in unterschiedlichen Bundles liegen?
    2. Was, wenn das Symbol von einem externen Package kommt, das mehrmals installiert ist (NPM und Abhängigkeits-Wahnsinn)?
    3. Was, wenn Module dynamisch geladen werden und nicht zur Compile-Zeit bekannt sind?

    In all diesen Fällen könnte man versehentlich mehrere Instanzen desselben “konzeptionellen” Symbols haben.

    // Bundle 1 hat eine Kopie von user-id-symbol
    const userId1 = Symbol("user.id");
    
    // Bundle 2 hat eine andere Kopie
    const userId2 = Symbol("user.id");
    
    // userId1 !== userId2, obwohl sie dasselbe sein sollten

    Die Lösung: Globale Symbol-Registry

    JavaScript bitet eine elegante Lösung: eine globale Registry für Symbole. Das ist eine Art “Telefonbuch”, das String-Keys auf Symbole mappt. Der JavaScript-Code einer Anwendung - egal in welchem Modul oder Bundle - teilt sich diese Registry.

    // Irgendwo im Bundle 1
    const userId1 = Symbol.for("app.user.id");
    
    // Irgendwo im Bundle 2
    const userId2 = Symbol.for("app.user.id");
    
    // Jetzt sind sie identisch
    console.log(userId1 === userId2);
    true

    Schauen wir uns ein Beispiel an, welches dieses Verhalten verdeutlichen würde. Dazu brauchen wir 3 Dateien, damit wir wirklich mit Modulen/Bundles die Lage nachstellen.

    Zuerst erstellen wir die erste Datei (bundleOne.js).

    bundleOne.js
    // --- bundleOne.js ---
    export const userId1 = Symbol.for("app.user.id");
    export const userToken1 = Symbol("app.user.token");

    Nun erstellen wir die zweite Datei (bundleTwo.js).

    bundleTwo.js
    // --- bundleTwo.js ---
    export const userId2 = Symbol.for("app.user.id");
    export const userToken2 = Symbol("app.user.token");

    Und nun verwenden wir diese beiden Dateien und versuchen die Symbole zu verlgiehcne. Das machen wir uns unserer main.js.

    main.js
    // --- main.js ---
    import { userId1, userToken1 } from "./bundleOne.js";
    import { userId2, userToken2 } from "./bundleTwo.js";
    
    console.log(userId1 === userId2);
    console.log(Symbol.keyFor(userId1));
    
    console.log("--- --- ---");
    
    console.log(userToken1 === userToken2);
    console.log(Symbol.keyFor(userToken1));
    true
    app.user.id
    --- --- ---
    false
    undefined

    Was passiert hier intern

    • Symbol.for("app.user.id") sucht in der globalen Symbol-Registry, ob ein Symbol mit diesem Key existiert.

      • Falls nicht vorhanden, wird es erstellt und eingetragen.
      • Falls vorhanden, wird dasselbe Symbol-Objekt zurückgegeben.
    • Symbol("app.user.token") erstellt immer ein neues, eindeutiges Symbol, das nicht global registriert ist.

    Das bedeutet: Der erste Aufruf von Symbol.for("app.user.id") erstellt das Symbol. Alle weiteren Aufrufe mit demselben String geben dasselbe Symbol zurück.


    Die Registry im Detail verstehen

    Die globale Registry ist konzeptionell eine Hash-Tabelle, die String-Keys auf Symbol-Objekte mappt. Man kann sie sich so vorstellen:

    +----------------------+---------------------+
    | String Key           | Symbol Pointer      |
    +----------------------+---------------------+
    | "app.user.id"        | -> Symbol @ 0x5000  |
    | "react.element"      | -> Symbol @ 0x5100  |
    | "lib.config.timeout" | -> Symbol @ 0x5200  |
    +----------------------+---------------------

    Wenn Symbol.for("app.user.id") aufgerufen wird:

    1. JavaScript hasht den String app.user.id zu einem Index in der Hash-Tabelle.
    2. JavaScript schaut an diesem Index nach.
    3. Falls ein Eintrag existiert, gib den Symbol-Pointer zurück.
    4. Falls kein Eintrag existiert:
      • Erstelle ein neues Symbol-Objekt
      • Speichere die Zuordnung (app.user.id => Symbol in der Registry)
      • Gib das neue Symbol zurück

    Wichtig: Diese Registry ist global im JavaScript-Laufzeit-Kontext. Dieser Fakt bedeutet:

    • In Browsern: Global über alle Scripte und iFrames hinweg (mit derselben Origin)
    • In NodeJS: Global im gesamten Prozess
    • In Web Workers: Jeder Worker hat seine eigene Registry

    Symbol.keyFor() - Die Rückrichtung

    Wenn man ein Symbol hat und wissen möchte, ob es in der globalen Registry ist und unter welchem Key, verwendet man Symbol.keyFor().

    Hier ein kleines Beispiel.

    Beispiel
    const globalSymbol = Symbol.for("myKey");
    const localSymbol = Symbol("myKey");
    
    console.log(Symbol.keyFor(globalSymbol));
    console.log(Symbol.keyFor(localSymbol));
    undefined

    Symbol.keyFor() macht folgendes.

    • Prüft, ob das Symbol ein “global registry symbol” ist (das wird intern geflagt)
      • Falls nein: Gib undefined zurück
      • Fall ja: Suche das Symbol in der Registry und gib den String-Key zurück

    Das Symbol-Objekt hat intern ein Flag, das sagt: “Ich bin ein globales Symbol”. Wenn dieses Flag gesetzt ist, durchsucht JavaScript die Registry nach diesem Symbol und gibt den zugehörigen String zurück.


    Lokale vs. globale Symbole

    Es ist wichtig, den Unterschied zwischen lokalen und globalen Symbolen zu verstehen.

    Lokales Symbol
    const localSymbol = Symbol("description");
    Globales Symbol
    const localSymbol = Symbol.for("description");

    Lokales Symbol

    • Wird mit Symbol() erstellt
    • Ist nur über die Variable zugänglich, in der es gespeichert ist
    • Nicht in der globalen Registry
    • Symbol.keyFor(localSymbol) gibt undefined zurück

    Globales Symbol

    • Wird mit Symbol.for() erstellt
    • Ist überall über Symbol.for("key") zugänglich
    • Ist in der globalen Registry gespeichert
    • Symbol.keyFor(globalSymbol) gibt den Registry-Key zurück

    Im Speicher unterscheiden sich diese durch ein internes Flag.

    Lokales Symbol-Objekt @ 0x5000:
    +---------------------------------+
    | Type Tag:       Symbol          |
    | Hash:           0x8A3F          |
    | Symbol ID:      23871           |
    | Flags:          0x00 <--------- | -- Global-Bit = 0
    | Description:    "description"   |
    +---------------------------------+
    
    Globales Symbol-Objekt @ 0x5100:
    +---------------------------------+
    | Type Tag:       Symbol          |
    | Hash:           0x7B2E          |
    | Symbol ID:      23872           |
    | Flags:          0x01 <--------- | -- Global-Bit = 1
    | Description:    "description"   |
    +---------------------------------+

    Das Global-Bit im Flags-Feld markiert ein Symbol als “Teil der globalen Registry”.


    Best Practices für Symbol.for()

    1. Verwende namespaced Keys

    Auch, wenn Symbole Kollisionen vermeiden sollen, gilt das nur für Symbol(). Bei Symbol.for() gibt es ein globales Namespace-Problem: Wenn zwei Libraries zufällig denselben String verwenden, bekommen sie dasselbe Symbol.

    Beispiel
    // SCHLECHT - zu generisch - Kollisionsgefahr
    const id = Symbol.for("id");
    
    // GUT - eindeutiger Namespace
    const id = Symbol.for("myapp.models.user.id");

    2. Verwende Symbol.for() nur wenn nötig

    Symbol.for() sollte nur verwendet werden, wenn man wirklich ein geteiltes Symbol braucht. Für interne Zwecke ist ein normales, lokales Symbol besser.

    Beispiel
    // Für interne, lokale Zwecke
    const internal = Symbol("internal");
    
    // Für Cross-Modul-Kommunikation
    const shared = Symbol.for("mylib.public.id");

    3. Dokumentiere globale Symbole

    Weil globale Symbole überall zugänglich sind, sollten sie gut dokumentiert werden.

    Beispiel
    /**
     * Globales Symbol für User-IDs in der gesamten Anwendung.
     * Wird von Auth, User-Service und DB-Layer verwendet.
     * Registry-Key: "app.user.id"
     */
    export const USER_ID = Symbol.for("app.user.id");

    Praktisches Beispiel zur Veranschaulichung

    In diesem Beispiel versuche ich zu zeigen, wann ein Symbol.for() nützlich sein kann.

    Für dieses Beispiel benötigen wir in einem Ordner folgende drei Dateien.

    • pluginApi.js
    • pluginImplementation.js
    • main.js
    pluginApi.js
    const PLUGIN_INTERFACE = Symbol.for("app.plugin.interface");
    
    export function registerPlugin(plugin) {
        if (!plugin[PLUGIN_INTERFACE]) {
            throw new Error("Das Plugin implementiert nicht die Schnittstelle");
        }
    
        const iface = plugin[PLUGIN_INTERFACE];
        console.log("✅ Plugin erkannt:", iface.version);
    
        iface.init();
        
        console.log("📦 Plugin registriert!");
    }
    pluginImplementation.js
        const PLUGIN_INTERFACE = Symbol.for("app.plugin.interface");
    
        export const myPlugin = {
            name: "PluginOne",
            version: "1.0.2",
    
            [PLUGIN_INTERFACE]: {
                version: "1.0.2",
                init() {
                    console.log("🛠️ Plugin initialisiert");
                },
                destroy() {
                    console.log("🗑️ Plugin beendet");
                }
            }
        };

    Und nun werden wir diese beiden Dateien in unserer main.js verwenden.

    main.js
    import { registerPlugin } from "./plugin_api.js";
    import { myPlugin } from "./plugin_implementation.js";
    
    const sym1 = Symbol.for("app.plugin.interface");
    const sym2 = Symbol.for("app.plugin.interface");
    
    console.log("Symbole gleich?", sym1 === sym2);
    
    registerPlugin(myPlugin);
    
    console.log("Hat Symbol-Interface:", myPlugin.hasOwnProperty(sym1));
    
    console.log("Interface-Version:", myPlugin[sym1].version);
    
    myPlugin[sym1].destroy();
    Symbole gleich? true
    ✅ Plugin erkannt: 1.0.2
    🛠️ Plugin initialisiert
    📦 Plugin registriert!
    Hat Symbol-Interface: true
    Interface-Version: 1.0.2
    🗑️ Plugin beendet

    Beide Dateien können das Symbol verwenden, ohne voneinander abhängig zu sein. Sie sind nur über den String app.plugin.interface verbunden.

    Well-Known Symbols - Meta-Programmierung

    Was sind Well-Known Symbols?

    Well-Known Symbols sind eine der mächtigsten und gleichzeitig am wenigsten verstandenen Features von JavaScript. Sie sind vordefinierte Symbole, die vom JavaScript-Standard selbst bereitgestellt werden. Diese Symbole ermöglichen es uns, das fundametale Verhalten von Objekten anzupassen - Ding, die normalerweise fest in der Sprache eingebaut sind.

    Man könnte sagen: Well-Known Symbols sind “Hooks” in die JavaScript-Engine. Sie sind Schnittstellen, über die man der Engine sagen kann: “Hey, wenn du mit Objekten arbeitest, mach es anders als normal”.

    Diese Symbole sind als statische Properties auf dem Symbol-Konstruktor verfügbar.

    console.log(Symbol.iterator);
    console.log(Symbol.toStringTag);
    console.log(Symbol.toPrimitive);
    
    // ... und viele mehr
    Symbol(Symbol.iterator)
    Symbol(Symbol.toStringTag)
    Symbol(Symbol.toPrimitive)

    Warum sind sie Symbole?

    Die JavaScript-Spec Designer hätten diese Features auch als normale String-Properties implementieren können, etwa obj.__iterator__ oder obj.__toStringTag__. Das hätte allerdings zwei Probleme:

    1. Kollisionsgefahr: Jemand könnte versehentlich eine Property mit demselben Namen erstellen.
    2. Verschmutzung: Diese “magischen” Properties würden in Object.keys() auftauchen und normale Enumeration stören.

    Durch die Verwendung von Symbolen sind diese “Hooks” unsichtbar bei normaler Enumeration, aber trotzdem zugänglich wenn man sie braucht.


    Symbol.iterator - Der Herzschlag der Iteration

    Das wichtigste und am häufigsten verwendete Well-Known Symbol ist Symbol.iterator. Es definiert, wie ein Objekt iteriert werden kann - also wie die for...of-Schleife, der Spread-Operator ... und andere Iterator-Konsumenten mit dem Objekt umgehen.

    Das Problem, das Symbol.iterator löst

    JavaScript hat viele verschiedene Arten von iterierbaren Dingen: Arrays, Strings, Maps, Sets, NodeLists und mehr. Aber wie soll JavaScript wissen, wie es über ein beliebiges Objekt iterieren soll?

    Über das nachstehende Objekt können wir nicht einfach so drüber iterieren.

    Beispiel
    const myData = {
        users: ["John", "Tom", "Anna"],
        posts: ["Post 1", "Post 2"]
    };
    
    for (let item of myData) { console.log(item); }
    TypeError: myData is not iterable

    Das Problem: JavaScript weiß nicht, wie es über dieses benutzerdefinierte Objekt iterieren soll. Soll es über die Keys iterieren? Über die Values? Über beide? In welcher Reihenfolge?

    Mit Symbol.iterator kann man das selbst festlegen.

    Beispiel
    const myData = {
        users: ["John", "Tom", "Anna"],
        posts: ["Post 1", "Post 2"],
    
        [Symbol.iterator]() {
            // Definiert, wie iteriert werden soll.
            const allItems = [...this.users, ...this.posts];
            let index = 0;
    
            return {
                next() {
                    if (index < allItems.length) {
                        return {
                            value: allItems[index++],
                            done: false
                        };
                    } else {
                        return {
                            done: true
                        };
                    }
                }
            };
        }
    };
    
    // Jetzt funktioniert es
    for (let item of myData) {
        console.log(item);
    }
    John
    Tom
    Anna
    Post 1
    Post 2

    Hier haben wir definiert, dass wir zuerst über die Benutzer (users) und dann über die Beiträge (posts) iterieren möchten.

    Wie funktioniert for...of intern?

    Wenn JavaScript eine for...of Schleife sieht, passiert folgendes:

    for (let item of myData) {
        console.log(item);
    }
    1. JavaScript sucht nach myData[Symbol.iterator]
    2. Falls diese Property nicht existiert => TypeError: myData is not iterable
    3. Falls sie existiert: JavaScript ruft myData[Symbol.iterator]() auf
    4. Das muss ein Iterator-Objekt zurückgeben (ein Objekt mit einer next() Methode)
    5. JavaScript ruft wiederholt iterator.next() auf
    6. next() muss ein Objekt mit { value: ..., done: ... } zurückgeben
    7. Falls done den Wert true hat, beende die Schleife (Loop)
    8. Falls done den Wert false hat, verwende value als Loop-Variable und iteriere weiter

    Das kann man sich vereinfacht so vorstellen:

    Vereinfachung
    const iterator = myData[Symbol.iterator]();
    
    while (true) {
        const result = iterator.next();
    
        if (result.done) break;
        
        const item = result.value;
        console.log(item);
    }

    Generator-Funktionen und Symbol.iterator

    Generator-Funktionen (Funktion mit function*) machen die Implementierung von Iteratoren viel einfacher.

    Beispiel
    const myData = {
        users: ["John", "Tom", "Anna"],
        posts: ["Post 1", "Post 2"],
    
        *[Symbol.iterator]() {
            // Viel einfacher mit Generator
            yield* this.users;
            yield* this.posts;
        } 
    };
    
    for (let item of myData) {
        console.log(item);
    }
    John
    Tom
    Anna
    Post 1
    Post 2

    Generator-Funktionen geben automatisch ein Iterator-Objekt zurück, das das next()-Protokoll implementiert. Jedes yield pausiert die Funktion und gibt einen Wert zurück.

    Was passiert hier?

    • yield* this.users ist Syntax-Zucker für “yield jeden Wert aus dem users-Array”
    • Die Generator-Funktion merkt sich intern, wo sie war
    • Bei jedem Aufruf von next() läuft sie bis zum nächsten yield weiter
    • Am Ende gibt sie automatisch { done: true } zurück

    Spread-Operator und Destrukturisierung

    Alles, was Symbol.iterator implementiert, funktioniert auch mit dem Spread-Operator und Destrukturisierung.

    Beispiel
    const myData = {
        users: ["John", "Tom", "Alice"],
        posts: ["Post 1", "Post 2"],
    
        *[Symbol.iterator]() {
            yield* this.users;
            yield* this.posts;
        }
    };
    
    const allItems = [...myData];
    console.log(allItems);
    
    const [first, second, ...rest] = myData;
    console.log(first);
    console.log(second);
    console.log(rest);
    [ 'John', 'Tom', 'Alice', 'Post 1', 'Post 2' ]
    John
    Tom
    [ 'Alice', 'Post 1', 'Post 2' ]

    Beide verwenden intern den Symbol.iterator Mechanismus.

    Praktisches Beispiel - Range Iterator

    Hier ein klassisches Beispiel - ein Range-Objekt, das über Zahlen iteriert.

    Range Iterator
    class Range {
        constructor(start, end) {
            this.start = start;
            this.end = end;
        }
    
        *[Symbol.iterator]() {
            for (let i = this.start; i <= this.end; i++) {
                yield i;
            }
        }
    }
    
    const range = new Range(1, 5);
    
    for (let num of range) {
        console.log(num);
    }
    
    console.log([...range]);
    1
    2
    3
    4
    [ 1, 2, 3, 4 ]

    Diese Lösung ist elegant und lesbar. Ohne Symbol.iterator müssten wir ein Array erstellen und zurückgeben, was unnötigen Speicher verbraucht.


    Symbol.toStringTag - Objekt-Identifikation

    Wenn man Object.prototype.toString.call(obj) aufruft, bekommt man einen String wie [object Type]. Der “Type”-Teil wird durch Symbol.toStringTag bestimmt.

    Das Problem

    Standardmäßig haben alle Objekte denselben Typ.

    Problem
    class User {}
    class Product {}
    
    const user = new User();
    const product = new Product();
    
    console.log(Object.prototype.toString.call(user));
    console.log(Object.prototype.toString.call(product));
    [object Object]
    [object Object]

    Beide sagen nur “Object”, was nicht sehr hilfreich ist. Man kann nicht unterscheiden, was für ein Objekt es ist.

    Die Lösung

    Mit Symbol.toStringTag kann man einen benutzerdefinierten Tag setzen.

    Lösung
    class User {
        get [Symbol.toStringTag]() {
            return "User";
        }
    }
    
    class Product {
        get [Symbol.toStringTag]() {
            return "Product";
        }
    }
    
    const user = new User();
    const product = new Product();
    
    console.log(Object.prototype.toString.call(user));
    console.log(Object.prototype.toString.call(product));
    [object User]
    [object Product]

    Jetzt kann man die Objekt unterscheiden.

    Wie Object.prototype.toString() funktioniert

    Intern macht Object.prototype.toString() ungefähr Folgendes.

    Schema-Beispiel
    // Vereinfachte Version
    Object.prototype.toString = function() {
        // 1. Versuche Symbol.toStringTag zu lesen
        const tag = this[Symbol.toStringTag];
    
        // 2. Falls vorhanden und ein String - verwende ihn
        if (typeof tag === "string") {
            return `[object ${tag}]`;
        }
    
        // 3. Sonst verwende den internen [[Class]]-Wert
        // z.B. "Object", "Array", "Function"
        const internalClass = getInternalClass(this);
        return `[object ${internalClass}]`;
    }

    Wo kann es nützlich sein?

    Beispielsweise für Bibliotheken und Frameworks, um deren spezifische Objekte identifizierbar zu machen.

    Beispiel
    class ValidationError extends Error {
        get [Symbol.toStringTag]() {
            return "ValidationError";
        }
    
        toString() {
            return `[object ${this[Symbol.toStringTag]}]`;
        }
    }
    
    const err = new ValidationError("Invalid input");
    
    console.log(err.toString());
    console.log(`Fehler aufgetreten: ${err}`);
    [object ValidationError]
    Fehler aufgetreten: [object ValidationError]

    Wichtig: In diesem Beispiel brauchen wir die toString() Methode direkt an der Klasse zu definieren, da sonst das Ergebnis anders aussehen wird, da wir die Error Klasse erweitern.

    Würden wir nicht die Klasse Error erweitern, könnten wir ohne Umwege es schreiben/anwenden.

    Beispiel (ohne Error)
    class ValidationError {
        get [Symbol.toStringTag]() {
            return "ValidationError";
        }
    }
    
    const err = new ValidationError("Invalid input");
    
    console.log(err.toString());
    console.log(`Fehler aufgetreten: ${err}`);
    [object ValidationError]
    Fehler aufgetreten: [object ValidationError]

    Built-in Beispiele

    Viele Built-in Objekte verwenden Symbol.toStringTag.

    Beispiel - Built-ins
    console.log(Object.prototype.toString.call(new Map()));
    console.log(Object.prototype.toString.call(new Set()));
    console.log(Object.prototype.toString.call(new Promise(() => {})));
    console.log(Object.prototype.toString.call(Math));
    console.log(Object.prototype.toString.call(JSON));
    [object Map]
    [object Set]
    [object Promise]
    [object Math]
    [object JSON]

    Alle haben intern Symbol.toStringTag gesetzt.


    Symbol.toPrimitive - Kontrolle über Type Coercion

    Das vielleicht mächtigste Well-Known Symbol ist Symbol.toPrimitive. Es gibt dir vollständige Kontrolle darüber, wie dein Objekt in einen primitiven Wert konvertiert wird.

    Das Type-Coercion-Problem

    JavaScript konvertiert automatisch zwischen Typen, was manchmal verwirrend ist.

    const obj = { value: 42 };
    
    console.log(obj + 10);
    console.log(+obj);
    [object Object]10
    NaN

    Bei beiden Ausgaben ist es sicherlich nicht das, was unsere Absicht gewesen wäre. JavaScript weiß allerdings nicht, wie ein Objekt in eine Zahl oder einen String konvertieren soll.

    Die Standard-Konvertierung

    Ohne Symbol.toPrimitive verwendet JavaScript einen zweistufigen Prozess.

    1. Für Number-Kontext: Versuche valueOf(), dann toString()
    2. Für String-Kontext: Versuche toString(), dann valueOf()
    Beispiel
    const obj = {
        valueOf() {
            return 42;
        },
        toString() {
            return "Zweiundvierzig"
        }
    };
    
    // valueOf wird verwendet
    console.log(+obj);
    
    // toString wird verwendet
    console.log(`${obj}`);
    
    // valueOf wird verwendet
    console.log(obj + 0);
    
    // valueOf wird verwendet, dann zu String konveriert
    console.log(obj + "");
    42
    Zweiundvierzig
    42
    "42"

    Das ist ein wenig kompliziert und nicht immer intuitiv.

    Symbol.toPrimitive gibt uns die Kontrolle

    Mit Symbol.toPrimitive bekommt man einen einzigen Einstiegspunkt mit voller Information.

    Beispiel
    const obj = {
        [Symbol.toPrimitive](hint) {
            console.log("hint:", hint);
    
            if (hint === "number") {
                return 42;
            }
    
            if (hint === "string") {
                return "Zweiundvierzig";
            }
    
            return true;
        }
    };
    
    console.log(+obj);
    console.log(`${obj}`);
    console.log(obj + "");
    console.log(obj == true);
    hint: number
    42
    
    hint: string
    Zweiundvierzig
    
    hint: default
    "true"
    
    true

    Die 3 Hints:

    1. number: Klare numerische Operationen
      • +obj (unäres Plus)
      • obj - 5
      • obj * 2
      • Math.max(obj, 10)
    2. string: Klare String-Operationen
      • String(obj)
      • ${obj} (Template Literal)
      • Explizite String-Konvertierung
    3. default: Mehrdeutige Situationen
      • obj + value (könnte Zahl oder String sein)
      • obj == value
      • new Date(obj)

    Der “default” Hint wird verwendet, wenn JavaScript nicht sicher ist, ob eine Zahl oder ein String erwartet wird. In den meisten Fällen sollte man “default” wie “number” behandeln, es sei denn man hat einen guten Grund, etwas anderes zu tun.

    Praktisches Beispiel - Money-Klasse

    Hier ein Beispiel, in dem Symbol.toPrimitive ziemlich nützlich ist.

    Beispiel
    class Money {
    
        constructor(amount, currency = "EUR") {
            this.amount = amount;
            this.currency = currency;
        }
    
        [Symbol.toPrimitive](hint) {
            if (hint === "number") {
                return this.amount;
            }
    
            if (hint === "string") {
                return `${this.amount.toFixed(2) ${this.currency}}`;
            }
    
            return this.amount;
        }
    
    }
    
    const price = new Money(98.5);
    
    // Als Zahl: 98.5
    console.log(+price); // 98.5
    console.log(price * 2); // 197
    
    // Als String: "98.50 EUR"
    console.log(`Preis: ${price}`); // Preis: 98.50 EUR
    console.log(String(price)); // 98.50 EUR
    
    // Vergleich (default hint)
    console.log(price > 50); // true
    console.log(price == 98.5); // true
    
    // Mathematik
    const total = price + new Money(10.5);
    console.log(total); // 109
    98.5
    197
    Preis: 98.50 EUR
    98.50 EUR
    true
    true
    109

    Somit haben wir dieses Objekt so aufgestellt, dass es sich in mathematischen Kontexten wie eine Zahl und in String-Kontexten wie ein formatierter String verhält.

    Wie Engine Symbol.toPrimitive verwendet

    Wenn JavaScript ein Objekt zu einem Primitive konvertieren muss, läuft intern ein Algorithmus ab, der ungefähr so funktioniert:

    1. Prüfung: Ist der Wert bereits ein Primitive? Falls ja, gib ihn zurück.
    2. Symbol.toPrimitive suchen: Existiert obj[Symbol.toPrimitive]?
    3. Falls ja: Rufe die Methode mit dem passenden Hint auf.
    4. Validierung: Ist das Ergebnis ein Primitive? Falls ja, gib es zurück. Falls nein, wirf TypeError.
    5. Falls Symbol.toPrimitive nicht existiert: Verwende den Standard-Algorithmus (valueOf/toString)

    Der Schlüsselpunkt: Symbol.toPrimitive hat Priorität über valueOf und toString(). Wenn es existiert, werden die anderen Methoden ignoriert.


    Symbol.hasInstance - Instanceof anpassen

    Symbol.hasInstance gibt uns Kontrolle über den instanceof Operator.

    Der Standard instanceof

    Normalerweise prüft instanceof die Prototype-Chain.

    Beispiel
    class Animal {}
    class Dog extends Animal {}
    
    const dog = new Dog();
    
    console.log(dog instanceof Dog);
    console.log(dog instanceof Animal);
    console.log(dog instanceof Object);
    true
    true
    true

    Die Prüfung läuft die Prototype-Chain hoch und schaut, ob Dog.prototype, Animal.prototype, etc. in der Chain sind.

    Custom instanceof mit Symbol.hasInstance

    Man kann dieses Verhalten komplett überschreiben.

    Beispiel - Anpassung
    class MyArray {
        static [Symbol.hasInstance](instance) {
            return Array.isArray(instance);
        }
    }
    
    console.log([] instanceof MyArray);
    console.log({} instanceof MyArray);
    console.log([1, 2, 3] instanceof MyArray);
    true
    false
    true

    Obwohl [] nicht von MyArray erbt, gibt instanceof true zurück, weil wir die Prüfung selbst definiert haben.

    Hier ein weiteres Beispiel mit Type Checking.

    Beispiel
    class Numeric {
        static [Symbol.hasInstance](instance) {
            return typeof instance === "number" ||
                typeof instance === "bigint" ||
                instance instanceof Number;
        }
    }
    
    console.log(42 instanceof Numeric);
    console.log(42n instanceof Numeric);
    console.log(new Number(42) instanceof Numeric);
    console.log("42" instanceof Numeric);
    true
    true
    true
    false

    Das erlaubt bequeme Type-Checks für “duck-typed” Konzepte.

    Praktische Beispiele

    Private Properties (Bibliotheks-Kontext)

    Vor der #private Syntax wurden die Symbole für private Properties verwendet. Auch heute sind sie noch nützlich für Bibliotheken, die nicht auf neueste JS-Features angewiesen sein wollen/können.

    Beispiel
    const _balance = Symbol("balance");
    const _transactions = Symbol("transactions");
    
    class BankAccount {
    
        constructor(initialBalance) {
            this[_balance] = initialBalance;
            this[_transactions] = [];
        }
    
        deposit(amount) {
            if (amount <= 0) {
                throw new Error("Menge muss positiv sein");
            }
            this[_balance] += amount;
            this[_transactions].push({
                type: "deposit",
                amount,
                date: new Date()
            });
        }
    
        withdraw(amount) {
            if (amount > this[_balance]) {
                throw new Error("Nicht genügend Guthaben");
            }
            this[_balance] -= amount;
            this[_transactions].push({
                type: "withdraw",
                amount,
                date: new Date()
            });
        }
    
        get balance() {
            return this[_balance];
        }
    
        getTransactionHistory() {
            return [...this[_transactions]];
        }
    
    }
    
    const account = new BankAccount(1000);
    account.deposit(400);
    account.withdraw(300);
    
    console.log("New balance:", account.balance);
    
    // Nicht zugänglich von außen
    console.log("account._balance", account._balance);
    
    // Zugänglich, weil im gleichen Scope
    console.log("account[_balance]", account[_balance]);
    
    // Aber sichtbar für Debuggung
    const symbols = Object.getOwnPropertySymbols(account);
    console.log(account[symbols[0]]);
    New balance: 1100
    account._balance => undefined
    account[_balance] => 1100
    1100

    Metadaten ohne Kollision

    In modernen Frameworks ist es üblich, Metadaten an Objekten zu speichern. Symbole sind sehr gut dafür geeignet.

    Beispiel
    // React-ähnliches System
    const ELEMENT_TYPE = Symbol.for("react.element");
    const MEMO_TYPE = Symbol.for("react.memo");
    const FORWARD_REF_TYPE = Symbol.for("react.forward_ref");
    
    function createElement(type, props, children) {
        return {
            $$typeof: ELEMENT_TYPE,
            type,
            props: { ...props, children }
        };
    }
    
    function memo(component) {
        return {
            $$typeof: MEMO_TYPE,
            type: component
        };
    }
    
    function isValidElement(obj) {
        return (
            typeof obj === "object" &&
            obj !== null &&
            obj.$$typeof === ELEMENT_TYPE
        );
    }
    
    function isMemoComponent(obj) {
        return (
            typeof obj === "object" &&
            obj !== null &&
            obj.$$typeof === MEMO_TYPE
        );
    }
    
    // Verwendung
    const MyComponent = () => createElement("div", {}, "Hello");
    const MemoizedComponent = memo(MyComponent);
    
    console.log(isValidElement(MyComponent()));
    console.log(isMemoComponent(MemoizedComponent));
    true
    true

    Das $$typeof verhindert, dass User-Daten als React-Elemente interpretiert werden.


    Iterierbares Objekt

    Beispiel
    const company = {
        name: "Sample company",
        employees: ["John", "Tom", "Alice"],
        [Symbol.iterator]() {
            let i = 0;
            const employees = this.employees;
            return {
                next() {
                    if (i < employees.length) {
                        return {
                            value: employees[i++],
                            done: false
                        };
                    } else {
                        return {
                            done: true
                        };
                    }
                }
            };
        }
    };
    
    for (const name of company) {
        console.log(name);
    }
    John
    Tom
    Alice

    In diesem Beispiel haben wir eigene Art und Weise definiert, wie über das Objekt iteriert werden kann.


    Kontrolle über Typ-Konvertierung

    Beispiel
    const product = {
        name: "Laptop",
        price: 1200,
        [Symbol.toPrimitive](hint) {
            if (hint === "string") return this.name;
            if (hint === "number") return this.price;
            return `${this.name} ${this.price} €`;
        }
    };
    
    console.log(String(product));
    console.log(+product);
    console.log(product + "");
    Laptop
    1200
    Laptop 1200 €

    Hiermit haben wir verschiedene Möglichkeiten geschaffen, ein Objekt zu einer “primitiven”, oder besser gesagt “einfachen”, Ausgabe zu konvertieren.


    Interne Flags

    Beispiel
    const IS_INTERNAL = Symbol("internal");
    
    function createUser(name) {
        return {
            name,
            role: "user",
            [IS_INTERNAL]: true
        };
    }
    
    const user = createUser("John");
    
    console.log(Object.keys(user));
    console.log(user[IS_INTERNAL]);
    [ 'name', 'role' ]
    true

    Wir können Symbole dafür verwenden, um bestimmte Eigenschaften von Standard-Verarbeitung wie Object.keys (also Standard-Sichtbarkeit) zu verstecken.