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.
Inhaltsverzeichnis
- 1. Das Problem mit der Unterstrich-Konvention
- 2. Grundlegende Syntax: Felder und Methoden mit #
- 3. Echte Kapselung: unzugaenglich selbst fuer Reflection
- 4. Private Methoden fuer interne Hilfslogik, die nicht Teil der API ist
- 5. Sichere Brand-Checks mit dem in-Operator
- 6. Statische private Felder und Methoden
- 7. Vergleich mit fruaeheren Kapselungsmustern: Closures und WeakMap
- 8. Vererbung: private Felder sind nicht fuer Subklassen sichtbar
- 9. Praxisbeispiel: ein Konto-Objekt mit erzwungenen Invarianten
- 10. Zusammenfassung
- 11. FAQ
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.