
Das Singleton garantiert, dass eine Klasse nur eine einzige Instanz hat, und bietet einen globalen Zugriffspunkt darauf. In modernem JavaScript ist der einfachste Weg ein privates statisches Feld und eine Methode getInstance(): static #instance; static getInstance() { return Database.#instance ??= new Database(); }. Ein ES-Modul, das ein Objekt exportiert, ist bereits ein Singleton: oft ist das die beste Antwort.
Das Singleton ist wohl das bekannteste und meistdiskutierte Design Pattern. Sein Versprechen ist einfach: eine Klasse, von der es nur eine einzige Instanz geben kann, überall erreichbar. Eine Datenbankverbindung, ein Logger, die Konfiguration der Anwendung sind die klassischen Beispiele. In JavaScript steht es in wenigen Zeilen, und die Sprache liefert sogar eine Alternative gratis mit: das Modul. Hier sind die Implementierungen, die heute funktionieren, samt ihren Fallstricken.
Das Singleton in einem Satz
Ein Singleton verhindert die Erzeugung mehrerer Instanzen einer Klasse und bietet einen einzigen Zugriffspunkt auf die vorhandene Instanz. Der erste Aufruf erzeugt das Objekt, die folgenden geben dasselbe zurück. Man nimmt es, wenn eine Ressource wirklich geteilt werden muss: zehn Verbindungen zu derselben Datenbank zu öffnen, weil zehn Module new Database() aufgerufen haben, wäre Verschwendung, und ein auf zehn Dateien verteiltes Log wäre unbrauchbar.
Abzuraten ist davon, sobald es nur dazu dient, einen Parameter nicht durchreichen zu müssen: dann ist es eine verkappte globale Variable, schwer zu testen und schwer zu ersetzen. Am Ende des Artikels kommen wir darauf zurück.
Implementierung 1: Klasse mit privatem statischem Feld
Das ist die empfohlene moderne Schreibweise. Das Feld #instance ist privat und statisch: es gehört der Klasse, nicht den Objekten, und niemand kann es von außen lesen oder überschreiben. getInstance() erzeugt die Instanz beim ersten Aufruf, dank des Operators ??= („zuweisen, wenn nullish“).
class Database {
static #instance;
constructor(dsn) {
this.dsn = dsn;
this.connectedAt = new Date();
}
static getInstance(dsn = 'mongodb://localhost') {
Database.#instance ??= new Database(dsn);
return Database.#instance;
}
query(sql) {
return `${this.dsn} > ${sql}`;
}
}
const a = Database.getInstance('mongodb://prod');
const b = Database.getInstance('mysql://autre');
console.log(a === b); // true : dasselbe Objekt
console.log(b.dsn); // 'mongodb://prod' : der zweite dsn wurde ignoriertDie letzte Zeile zeigt einen Fallstrick des Patterns: die Parameter des zweiten Aufrufs gehen stillschweigend verloren. Nimmt dein Singleton Parameter entgegen, kommen sie entweder aus einer einzigen Quelle (der Konfiguration), oder getInstance() wirft einen Fehler, sobald abweichende übergeben werden.
Noch hindert nichts daran, direkt new Database() zu schreiben. JavaScript kennt keinen privaten Konstruktor; man ahmt ihn mit einem Token nach, das nur die Klasse selbst kennt:
const cle = Symbol('Database');
class Database {
static #instance;
constructor(jeton) {
if (jeton !== cle) {
throw new Error('Utilisez Database.getInstance()');
}
}
static getInstance() {
return (Database.#instance ??= new Database(cle));
}
}
new Database(); // Error: Utilisez Database.getInstance()Implementierung 2: der Konstruktor gibt die vorhandene Instanz zurück
Das ist die ursprüngliche Fassung dieses Artikels, und sie funktioniert weiterhin: gibt ein Konstruktor ausdrücklich ein Objekt zurück, liefert new dieses Objekt statt des neuen. Die Instanz wird in einer statischen Eigenschaft gemerkt.
class Database {
constructor(data) {
if (Database.instance) {
return Database.instance; // new gibt das vorhandene Objekt zurück
}
this._data = data;
Database.instance = this;
}
getData() {
return this._data;
}
setData(data) {
this._data = data;
}
}
const mongo = new Database('mongo');
console.log(mongo.getData()); // 'mongo'
const mysql = new Database('mysql');
console.log(mysql.getData()); // 'mongo' : es ist dasselbe Objekt wie mongo
console.log(mongo === mysql); // trueIhr Vorteil: die Schreibweise new Database() bleibt für den aufrufenden Code unverändert. Ihr Nachteil: sie überrascht. Ein new, das nichts erzeugt, widerspricht der Erwartung beim Lesen, und Database.instance ist öffentlich, also veränderbar. Die Fassung mit privatem Feld ist eindeutiger.
Implementierung 3: ein ES-Modul ist bereits ein Singleton
Ein JavaScript-Modul wird pro Anwendung nur einmal ausgewertet, egal wie viele Dateien es importieren. Eine Instanz zu exportieren reicht also für ein Singleton, ohne Klasse und ohne getInstance().
const config = Object.freeze({
env: process.env.NODE_ENV ?? 'development',
apiUrl: 'https://api.example.com',
});
export default config;class Logger {
#lines = [];
log(message) {
this.#lines.push(`${new Date().toISOString()} ${message}`);
}
get count() {
return this.#lines.length;
}
}
export const logger = new Logger(); // eine einzige Instanz für die ganze Anwendungimport { logger } from './logger.js';
import config from './config.js';
logger.log(`Démarrage en ${config.env}`);Jede Datei, die logger importiert, bekommt dasselbe Objekt. In den meisten Projekten ist das die bevorzugte Lösung: lesbar, ohne Magie, und das Modulsystem garantiert die Einzigartigkeit. Zwei Grenzen: die Instanz entsteht beim Laden des Moduls, auch wenn sie niemand benutzt, und das Singleton ist nur je Dateipfad eindeutig (zwei Kopien eines Pakets in node_modules ergeben zwei Instanzen).
Die TypeScript-Fassung
TypeScript ergänzt, was JavaScript fehlt: einen wirklich private Konstruktor, der new schon beim Kompilieren verbietet.
class Database {
private static instance: Database | undefined;
private constructor(private readonly dsn: string) {}
static getInstance(dsn = 'mongodb://localhost'): Database {
Database.instance ??= new Database(dsn);
return Database.instance;
}
query(sql: string): string {
return `${this.dsn} > ${sql}`;
}
}
const db = Database.getInstance();
// new Database('x'); // Fehler TS2673: der Konstruktor ist privatDas Gegenstück in PHP
Die Struktur ist dieselbe: statische Eigenschaft, privater Konstruktor, Methode getInstance(). PHP fügt zwei nützliche Riegel hinzu, ein privates __clone und ein __wakeup, das die Deserialisierung verweigert, damit keine Kopie entstehen kann.
final class Database
{
private static ?Database $instance = null;
private function __construct(private readonly string $dsn)
{
}
public static function getInstance(string $dsn = 'mysql:host=localhost'): Database
{
return self::$instance ??= new self($dsn);
}
public function query(string $sql): string
{
return "{$this->dsn} > {$sql}";
}
private function __clone()
{
}
public function __wakeup(): void
{
throw new LogicException('Un singleton ne se désérialise pas.');
}
}
$a = Database::getInstance('mysql:host=prod');
$b = Database::getInstance();
var_dump($a === $b); // bool(true)
echo $a->query('SELECT 1'); // mysql:host=prod > SELECT 1Wann du kein Singleton nehmen solltest
Das Pattern wird aus guten Gründen kritisiert, und die sollte man kennen, bevor man es einsetzt:
- Es versteckt die Abhängigkeiten. Eine Funktion, die mitten im Code
Database.getInstance()aufruft, hängt von der Datenbank ab, ohne das in ihrer Signatur zu sagen. Die Verbindung als Parameter zu übergeben (Dependency Injection) macht die Abhängigkeit sichtbar und austauschbar. - Es erschwert die Tests. Der Zustand überlebt von einem Test zum nächsten. Wenn du beim Singleton bleibst, ergänze eine nur für Tests gedachte Methode
static reset(), oder teste über die Injection. - Es ist globaler Zustand. Alles, was für globale Variablen gilt, gilt auch für das Singleton: Kopplung, Fernwirkungen, Reihenfolge der Initialisierung.
Die Faustregel: ein Modul, das eine Instanz exportiert (Implementierung 3), für die Konfiguration und für zustandslose Helfer; Dependency Injection für alles, was eine externe Ressource berührt; und die Klasse mit getInstance(), wenn eine Bibliothek diesen einen Zugriffspunkt vorschreibt. Das Guard-Clause-Pattern und die Klassen in JavaScript führen das Thema auf der Seite der Code-Struktur weiter.
Häufige Fehler
static reset() vor, oder injiziere die Abhängigkeit, statt im Fachcode getInstance() aufzurufen.new Database('mysql') gibt die mit „mongo“ erzeugte Instanz zurück: die Argumente des zweiten Aufrufs gehen ohne Warnung verloren. Dokumentiere das, oder wirf einen Fehler, wenn die Parameter abweichen.getInstance().

