Private Class Fields: echte Kapselung in JavaScript statt Unterstrich-Konvention
AI generated
JS
() =>
JavaScript · Klassen · Kapselung
Private Class Fields
Echte Kapselung in JavaScript-Klassen statt der Unterstrich-Konvention

Ein Feldname mit fuehrendem Unterstrich wie _wert war in JavaScript nie mehr als eine Konvention: von aussen jederzeit lesbar, ueberschreibbar und in jedem Debugger sichtbar. Private Klassenfelder und -methoden mit der #-Syntax setzen echte, vom Interpreter durchgesetzte Kapselung um, mit klaren Grenzen bei Vererbung und einem eigenen, sicheren Weg fuer Brand-Checks ueber den in-Operator.

14 Min. Lesezeit #feld · #methode() in-Operator Brand-Check

1. Das Problem mit der Unterstrich-Konvention

Jahrelang war ein fuehrender Unterstrich, etwa this._wert, das einzige verfuegbare Signal in JavaScript, um ein Feld als privat, also als reines Implementierungsdetail, zu kennzeichnen. Der Interpreter hat diese Konvention jedoch niemals durchgesetzt: Von aussen war _wert genauso lesbar, ueberschreibbar und aufzaehlbar wie jede oeffentliche Eigenschaft, das Sternchen war reine Absichtserklaerung an andere Entwicklerinnen und Entwickler, keine technische Barriere.

In der Praxis fuehrte das regelmaessig zu Problemen: Externer Code griff versehentlich oder bewusst direkt auf _wert zu, umging damit Validierungslogik in Settern und brach interne Invarianten der Klasse, ohne dass der Interpreter jemals eingegriffen haette. Private Klassenfelder mit der #-Syntax schliessen diese Luecke, indem sie Zugriff von ausserhalb der Klasse auf Sprachebene unmoeglich machen, nicht nur per Konvention.

2. Grundlegende Syntax: Felder und Methoden mit #

Ein privates Feld wird im Klassenkoerper mit vorangestelltem Rautezeichen deklariert, etwa #kontostand = 0. Diese Deklaration ist zwingend erforderlich, ein privates Feld kann nicht dynamisch zur Laufzeit hinzugefuegt werden, wie es bei oeffentlichen Eigenschaften per this['neuesFeld'] = wert moeglich waere. Der Zugriff innerhalb der Klasse erfolgt ganz normal ueber this.#kontostand.

Die gleiche Syntax funktioniert auch fuer Methoden: #berechneZinsen() definiert eine Methode, die ausschliesslich innerhalb der eigenen Klasse aufgerufen werden kann. Wichtig ist, dass der Name nach dem Rautezeichen strengen Bezeichnerregeln folgt und dass jede Referenz auf ein privates Feld, auch der Deklarationsname selbst, konsistent das Rautezeichen mitfuehren muss, es gibt keine Kurzschreibweise ohne #.


class Konto {
  #kontostand = 0;

  einzahlen(betrag) {
    this.#kontostand += betrag;
    this.#protokolliere('Einzahlung', betrag);
  }

  #protokolliere(aktion, betrag) {
    console.log(`${aktion}: ${betrag} EUR, neuer Stand: ${this.#kontostand}`);
  }
}

3. Echte Kapselung: unzugaenglich selbst fuer Reflection

Der entscheidende Unterschied zur Unterstrich-Konvention zeigt sich, sobald Code ausserhalb der Klasse versucht, auf ein privates Feld zuzugreifen: obj.#kontostand ist an dieser Stelle nicht einmal gueltige Syntax, der Parser wirft bereits beim Laden des Skripts einen SyntaxError, lange bevor der Code ueberhaupt ausgefuehrt wird. Das unterscheidet sich fundamental von einem TypeError zur Laufzeit, den man noch abfangen koennte.

Auch Reflection-Mechanismen wie Object.getOwnPropertyNames(), Object.keys() oder JSON.stringify() erfassen private Felder grundsaetzlich nicht, sie erscheinen in keiner Aufzaehlung und in keiner Serialisierung. Selbst Reflect.ownKeys() liefert private Felder nicht als regulaere Eintraege zurueck. Diese Unsichtbarkeit ist eine bewusste Design-Entscheidung, die private Felder von jeder Form nachtraeglicher Introspektion durch Bibliothekscode oder Debugging-Werkzeuge abschirmt.


const konto = new Konto();
konto.einzahlen(100);

console.log(Object.keys(konto));           // []
console.log(JSON.stringify(konto));         // {}
console.log(konto.#kontostand);              // SyntaxError beim Parsen

4. Private Methoden fuer interne Hilfslogik, die nicht Teil der API ist

Private Methoden eignen sich hervorragend, um interne Hilfslogik von der oeffentlichen API einer Klasse zu trennen. Waehrend eine oeffentliche Methode wie berechneGesamtsumme() Teil des stabilen Vertrags mit Aufrufern bleibt, kann eine private Hilfsmethode wie #rundeAufZweiStellen() jederzeit ohne Ruecksicht auf Abwaertskompatibilitaet umbenannt, aufgeteilt oder entfernt werden, weil sie fuer Aussenstehende gar nicht existiert.

Auch private Getter und Setter sind moeglich, etwa get #normalisiertesFormat(), was sich anbietet, wenn ein abgeleiteter Wert innerhalb der Klasse an mehreren Stellen gebraucht wird, aber niemals direkt von aussen gesetzt oder gelesen werden soll. Diese klare Trennung zwischen oeffentlicher Schnittstelle und privater Implementierung macht Refactorings innerhalb einer Klasse deutlich risikoaermer.


class Preisrechner {
  #steuersatz = 0.19;

  berechneGesamtsumme(nettoBetrag) {
    return this.#rundeAufZweiStellen(nettoBetrag * (1 + this.#steuersatz));
  }

  #rundeAufZweiStellen(zahl) {
    return Math.round(zahl * 100) / 100;
  }
}

5. Sichere Brand-Checks mit dem in-Operator

Ein haeufiges Problem bei generischem Code ist, sicher zu pruefen, ob ein beliebiges Objekt tatsaechlich eine Instanz einer bestimmten Klasse ist, ohne dabei einen Fehler zu riskieren, falls es das nicht ist. Der Ausdruck #feld in objekt loest genau das: Er gibt true zurueck, wenn objekt das private Feld #feld besitzt, und false, wenn nicht, ganz ohne einen TypeError zu werfen, selbst wenn objekt gar keine Instanz der Klasse ist.

Das ist besonders wertvoll fuer sogenannte Brand-Checks in Mixins oder Bibliotheks-Code, bei denen instanceof allein nicht ausreicht, weil instanceof durch manipulierte Prototypenketten getaeuscht werden kann, waehrend #feld in objekt untrueglich pruefft, ob das Objekt tatsaechlich durch den Konstruktor gelaufen ist, der dieses private Feld initialisiert.


class Konto {
  #kontostand = 0;

  static istKonto(objekt) {
    return #kontostand in objekt; // true, false, niemals ein Fehler
  }
}

console.log(Konto.istKonto(new Konto())); // true
console.log(Konto.istKonto({}));           // false, kein Fehler

6. Statische private Felder und Methoden

Neben Instanzfeldern koennen auch statische Felder und Methoden privat sein, etwa static #instanzZaehler = 0. Ein solches Feld gehoert nicht zu einer einzelnen Instanz, sondern zur Klasse selbst, und ist genauso unzugaenglich von aussen wie ein privates Instanzfeld, eignet sich also gut fuer klassenweiten internen Zustand wie Zaehler, Caches oder Konfigurationswerte.

Statische private Methoden werden haeufig fuer interne Factory-Logik verwendet, etwa static #validiereEingabe(daten), die von einer oeffentlichen statischen Fabrikmethode aufgerufen wird, aber selbst niemals direkt von aussen aufrufbar sein soll. Wichtig zu wissen: Innerhalb einer statischen Methode bezieht sich this auf die Klasse selbst, nicht auf eine Instanz, was beim Zugriff auf statische private Felder zu beachten ist.

7. Vergleich mit fruaeheren Kapselungsmustern: Closures und WeakMap

Vor privaten Klassenfeldern gab es zwei gaengige Wege, echte Kapselung in JavaScript nachzubilden. Der erste war eine Factory-Funktion mit Closures, bei der private Variablen als lokale Variablen der Funktion existierten und nur ueber zurueckgegebene Methoden erreichbar waren. Das funktioniert zuverlaessig, verzichtet aber komplett auf die class-Syntax und macht Vererbung ueber prototypische Ketten muehsam.

Der zweite Weg war eine modulweite WeakMap, die jede Instanz auf ein Objekt mit ihren privaten Daten abbildet, etwa privateDaten.set(this, { kontostand: 0 }). Das funktioniert mit class-Syntax, erzeugt aber zusaetzlichen Verwaltungsaufwand bei jedem Zugriff und einen gewissen Speicher-Overhead durch die zusaetzliche Map-Struktur, verglichen mit dem direkten, vom Interpreter optimierten Zugriff auf ein #-Feld.

8. Vererbung: private Felder sind nicht fuer Subklassen sichtbar

Anders als protected in Sprachen wie Java oder C# sind private Felder in JavaScript ausschliesslich innerhalb genau der Klasse sichtbar, in der sie deklariert wurden, auch fuer Subklassen bleiben sie unerreichbar. Eine Unterklasse, die von Konto erbt, kann this.#kontostand nicht direkt lesen oder schreiben, selbst wenn sie eine Instanz von Konto ist, weil das private Feld an die Klasse Konto gebunden ist, nicht an die Instanz allgemein.

Wird geschuetzter Zugriff fuer Unterklassen tatsaechlich gebraucht, bleibt nur der Umweg ueber eine geschuetzte Methode oder einen geschuetzten Getter ohne fuehrendes Rautezeichen, der das private Feld innerhalb der Basisklasse kontrolliert nach aussen reicht. Diese bewusste Design-Entscheidung erzwingt eine klarere Trennung zwischen echter Basisklassen-Implementierung und dem, was Unterklassen tatsaechlich erweitern duerfen sollen.

9. Praxisbeispiel: ein Konto-Objekt mit erzwungenen Invarianten

Ein Bankkonto ist ein anschauliches Beispiel, weil seine zentrale Invariante, ein Kontostand darf nie ohne kontrollierten Weg negativ werden, unbedingt geschuetzt werden muss. Mit einem privaten Feld #kontostand kann diese Regel ausschliesslich ueber die Methode abheben() durchgesetzt werden, ein direkter Schreibzugriff von aussen ist auf Sprachebene ausgeschlossen, nicht nur per Konvention verboten.

Kombiniert mit einer privaten Hilfsmethode fuer die Validierung und einem statischen Brand-Check per in-Operator entsteht eine Klasse, die ihre Invarianten unabhaengig davon garantiert, wie sorgfaeltig der aufrufende Code geschrieben ist, ein deutlicher Zuverlaessigkeitsgewinn gegenueber der alten Unterstrich-Konvention, bei der jeder externe Zugriff die Invariante haette brechen koennen.


class Konto {
  #kontostand;

  constructor(startguthaben = 0) {
    this.#kontostand = startguthaben;
  }

  abheben(betrag) {
    if (!this.#istGueltigerBetrag(betrag)) {
      throw new Error('Ungueltiger Betrag');
    }
    this.#kontostand -= betrag;
  }

  #istGueltigerBetrag(betrag) {
    return betrag > 0 && betrag <= this.#kontostand;
  }

  get kontostand() {
    return this.#kontostand; // kontrollierter Lesezugriff
  }
}
Ansatz Echte Kapselung Zugriff via Reflection moeglich Performance-Overhead
Unterstrich-Konvention (_feld) Nein, nur Absicht Ja, vollstaendig Keiner
Closures/Factory-Funktionen Ja Nein Gering, aber ohne class-Syntax
WeakMap pro Instanz Ja Praktisch nein Kleiner Overhead durch Map-Zugriff
Private Felder (#) Ja, vom Interpreter erzwungen Nein Minimal, vom Interpreter optimiert

Mironsoft

Moderne Browser-APIs, Performance und wartbares JavaScript

JavaScript, das im echten Browser robust bleibt, nicht nur im Tutorial?

Wir prüfen bestehenden Frontend-Code auf veraltete Patterns, unnötige Bibliotheken und Performance-Fallen und ersetzen sie durch moderne, native Browser-APIs, die weniger Bundle-Gewicht und weniger Wartungslast bedeuten.

Code-Review

Veraltete Patterns, unnötige Dependencies und Memory Leaks systematisch aufspüren.

Performance-Optimierung

Bundle-Größe, Ladezeit und Runtime-Performance mit modernen APIs verbessern.

Modernisierung

Native Browser-APIs statt schwerer Bibliotheken gezielt einführen.

10. Zusammenfassung

Private Class Fields: Das Wichtigste auf einen Blick

Kernidee

Das Rautezeichen macht Klassenfelder und -methoden auf Sprachebene unzugaenglich von ausserhalb der Klasse.

Unterschied zur Konvention

Ein fuehrender Unterstrich ist nur eine Absicht, das Rautezeichen wird vom Interpreter tatsaechlich erzwungen.

Brand-Check

Der Ausdruck #feld in objekt prueft sicher, ob ein Objekt eine echte Instanz ist, ganz ohne Fehlerrisiko.

Wichtige Grenze

Private Felder sind fuer Subklassen unsichtbar, geschuetzter Zugriff braucht eine explizite Getter-Methode.

11. FAQ: Private Class Fields: Das Wichtigste auf einen Blick

1Was ist der Unterschied zwischen _feld und #feld?
_feld ist reine Namenskonvention ohne technische Wirkung, das Feld bleibt von aussen normal lesbar und schreibbar. #feld dagegen wird vom JavaScript-Interpreter selbst durchgesetzt, ein Zugriff von ausserhalb der Klasse ist nicht einmal gueltige Syntax.
2Kann ich ein privates Feld dynamisch zur Laufzeit hinzufuegen?
Nein, private Felder muessen im Klassenkoerper deklariert werden, bevor sie genutzt werden koennen. Anders als bei oeffentlichen Eigenschaften ist es nicht moeglich, ein privates Feld erst spaeter per Zuweisung neu zu erzeugen.
3Erscheinen private Felder bei JSON.stringify oder Object.keys?
Nein, private Felder sind fuer alle Standard-Reflection- und Serialisierungsmechanismen unsichtbar, sie tauchen weder in Object.keys() noch in JSON.stringify() noch in Reflect.ownKeys() als regulaere Eintraege auf.
4Was passiert, wenn ich versehentlich auf ein privates Feld von aussen zugreife?
Der JavaScript-Parser wirft bereits beim Laden des Skripts einen SyntaxError, noch bevor der Code ueberhaupt ausgefuehrt wird, weil obj.#feld ausserhalb der deklarierenden Klasse gar keine gueltige Syntax darstellt.
5Wozu dient der Ausdruck #feld in objekt?
Er ermoeglicht einen sicheren Brand-Check: Er gibt true oder false zurueck, je nachdem ob objekt das private Feld besitzt, und wirft dabei niemals einen Fehler, selbst wenn objekt gar keine Instanz der betreffenden Klasse ist.
6Koennen Unterklassen auf private Felder der Basisklasse zugreifen?
Nein, private Felder sind ausschliesslich innerhalb der Klasse sichtbar, die sie deklariert. Eine Unterklasse muss stattdessen eine geschuetzte Methode oder einen Getter der Basisklasse verwenden, um kontrolliert auf den Wert zuzugreifen.
7Was ist der Unterschied zwischen privaten Feldern und der WeakMap-Technik?
Beide bieten echte Kapselung, aber private Felder werden direkt vom Interpreter verwaltet und optimiert, waehrend eine WeakMap-Loesung zusaetzlichen Verwaltungsaufwand bei jedem Zugriff sowie einen gewissen Speicher-Overhead durch die zusaetzliche Map-Struktur verursacht.
8Gibt es auch private statische Felder und Methoden?
Ja, mit static #feld beziehungsweise static #methode() lassen sich auch klassenweite, private Daten und Hilfslogik definieren, die genauso unzugaenglich von aussen sind wie private Instanzfelder.
9Muss ich bei jedem Zugriff das Rautezeichen mitschreiben?
Ja, es gibt keine Kurzschreibweise. Sowohl bei der Deklaration als auch bei jedem lesenden oder schreibenden Zugriff innerhalb der Klasse muss das Rautezeichen konsistent Teil des Bezeichners sein, etwa this.#feld.
10Lohnt sich der Umstieg von der Unterstrich-Konvention auf private Felder immer?
In den allermeisten Faellen ja, insbesondere fuer wirklich sicherheitsrelevante Invarianten. Bei sehr einfachen internen Hilfswerten ohne Risiko fuer Missbrauch von aussen ist der Unterschied in der Praxis oft gering, aber die klare Absicherung bleibt trotzdem meist die bessere Wahl.