Schließungen gegenüber privaten Eigenschaften und #: Was jede Art in TypeScript und JavaScript schützt
„private“ in TypeScript ist eine Prüfung zur Kompilierzeit, Hash-Felder werden zur Laufzeit durchgesetzt und Schließungen erfassen den lexikalischen Umfang. Ein Vergleich dazu, was jede dieser Mechanismen bewirkt.
Quellen: Closures vs Private in TypeScript: If We Have private and #, Why Do We Still Need Closures?
TypeScript verfügt über private und JavaScript über #; daher ist es berechtigt, sich zu fragen, warum trotzdem Schließfunktionen benötigt werden, um Variablen zu schützen. Die Antwort scheint offensichtlich, bis man untersucht, wie JavaScript zur Laufzeit funktioniert – dann wird es interessanter. Schließfunktionen kommen später. Zuerst: Was bewirkt private in TypeScript tatsächlich?
TypeScript private
Nehmen wir eine Klasse mit einer privaten Eigenschaft:
class User {
private secret: string = "hidden";
}
Erstellen Sie nun eine Instanz und lesen Sie die Eigenschaft von außerhalb der Klasse:
const me = new User();
console.log(me.secret);
Der entscheidende Satz ist dieser:
console.log(me.secret);
Der Compiler tsc lehnt es mit einem Fehler ab, wodurch die Variable als geschützt erscheint. Das Problem ist jedoch, dass private hauptsächlich eine Eigenschaft des Typsystems ist. Wenn TypeScript in JavaScript umgewandelt wird, überlebt das Schlüsselwort nicht als Laufzeit-Privatsphärenmechanismus. Dieser Quelltext:
class User {
private secret: string = "hidden";
}
kann ungefähr wie folgt ausgegeben werden:
class User {
constructor() {
this.secret = "hidden";
}
}
Zu diesem Zeitpunkt hat JavaScript keine Ahnung, dass secret als privat gedacht war, da es sich um eine gewöhnliche Eigenschaft handelt. Folglich:
const me = new User();
console.log(me.secret);
kann ausdrucken:
hidden
Der Unterschied ist wichtig. Was private dem Compiler mitteilt, ist, dass Code, der den Typ verwendet, die Eigenschaft nicht ändern darf. Es teilt dem Laufzeitumfeld jedoch nicht mit, dass die Eigenschaft unerreichbar sein muss – das sind völlig unterschiedliche Aussagen.
Was # ändert
TypeScript unterstützt außerdem JavaScripts eingebaute private Klassenfelder:
class User {
#secret: string = "hidden";
}
Der Versuch, das Feld jetzt von außen zu lesen, schlägt auf andere Weise fehl:
const me = new User();console.log(me.#secret);
Das ist nicht nur eine Compilerwarnung. Das Feld ist auch zur Laufzeit privat, denn die #-Syntax erzeugt ein echtes privates Klassenfeld. Es ist illegal, darauf von außerhalb der Klasse zuzugreifen:
console.log(me.#secret);
Auch das Lesen einer einfachen Eigenschaft mit demselben Namen erreicht sie nicht:
console.log(me.secret);
Da #secret keine gewöhnliche Eigenschaft mit dem Namen secret ist, könnte es verlockend sein, es sich als Zeichenketten-Schlüssel vorzustellen:
me["#secret"]
Aber das funktioniert nicht. Das # ist keine Namenskonvention, denn die Laufzeitumgebung verfügt über ein echtes Konzept von privaten Feldern. Wir haben daher zwei getrennte Konzepte. Das erste wird hauptsächlich durch das Typsystem vorgeschrieben:
private secret: string;
und der zweite durch die Semantik von JavaScript:
#secret: string;
Wo Schließungen zum Einsatz kommen
Bevor es private Klassenfelder gab, benötigten Entwickler dennoch Daten, auf die nicht einfach von außen zugegriffen werden konnte, und eines der mächtigsten Werkzeuge, die die Sprache bereits besaß, waren Schließungen. Hier ist eine Fabrikfunktion:
function createUser() {
let secret = "hidden";
Es gibt ein Objekt zurück, das einen einzigen Zugriffsmethoden bereitstellt:
return {
getSecret() {
return secret;
}
};
}const user = createUser();console.log(user.getSecret());
Die Aufrufung dieses Accessors gibt aus:
hidden
Achten Sie darauf, was fehlt. Es gibt keinen solchen Eigenschaftenzugriff:
user.secret
Das zurückgegebene Objekt verfügt nicht einmal über eine secret-Eigenschaft. Die Variable befindet sich im lexikalischen Umfeld, das beim Ausführen von createUser() erstellt wurde, und die zurückgegebene getSecret()-Funktion erinnert sich an dieses Umfeld. Das ist das Wesen einer Schließung: Eine Funktion behält den Zugriff auf Variablen aus dem Scope, in dem sie erstellt wurde, auch dann noch, wenn die äußere Funktion bereits abgeschlossen ist. Nach diesem Aufruf:
const user = createUser();
Die äußere Funktion ist abgeschlossen. Normalerweise würde man ihre lokale Variable erwarten:
let secret = "hidden";
verschwinden. Dennoch erreicht getSecret() es weiterhin, weil es sich auf secret bezieht – von dort stammt auch der Name.
Privater Zustand ohne Offenlegung
Closures können einen privaten Zustand ohne sichtbare Variablen speichern. Ein Zähler ist ein gutes Beispiel:
function myCounter() {
let count = 0;
return {
increment() {
count++;
}, getCount() {
return count;
}
};
}const counter = myCounter();counter.increment();
counter.increment();console.log(counter.getCount());
Wenn die Increment-Methode zweimal aufgerufen und anschließend die Anzahl abgefragt wird, wird ausgegeben:
2
während dies nicht funktioniert:
console.log(counter.count);
Denn count ist keine Eigenschaft des Zählers. Es handelt sich um eine Variable innerhalb der erfassten lexikalischen Umgebung. Das Objekt bietet das Verhalten, während der tatsächliche Zustand an einem anderen Ort aufbewahrt wird – eine Form der Verkapselung, die aus dem lexikalischen Scoping entsteht.
Ist # einfach besser?
Nicht unbedingt. Sie lösen verwandte Probleme, sind aber keine identischen Funktionen. Hier ist die Klassenversion:
class User {
#secret = "hidden";
getSecret() {
return this.#secret;
}
}
Darin ist #secret ein expliziter, privater Klassenzustand, der dem Objekt gehört, und die Laufzeit kennt ihn. In der Closure-Version:
function createUser() {
let secret = "hidden";
return {
getSecret() {
return secret;
}
};
}
Die Variable ist überhaupt keine Eigenschaft des Objekts, sondern befindet sich im umgebenden lexikalischen Kontext. Die Mechanismen sind grundlegend unterschiedlich. Ein #-Feld gibt an, dass dieses Objekt ein privates Feld besitzt, während eine Schließung zeigt, dass diese Funktion den Zustand aus ihrem umgebenden Scope nutzen kann. Dieser Unterschied prägt die API-Entwicklung.
Zustand, der zwischen Funktionen geteilt wird
Schließungen bieten mehr als nur Privatsphäre: Mehrere Funktionen können denselben Zustand teilen. Ein Bankkonto zeigt das beispielsweise:
function createBankAccount(initialBalance) {
let balance = initialBalance;
return {
deposit(amount) {
balance += amount;
}, withdraw(amount) {
balance -= amount;
}, getBalance() {
return balance;
}
};
}
So sieht die Verwendung aus:
const account = createBankAccount(1000);
account.deposit(500);console.log(account.getBalance());
Und das Ergebnis ist:
1500
Es gibt kein öffentliches Eigentum zum Lesen:
account.balance
deposit(), withdraw() und getBalance() greifen alle auf dieselbe lokale Variable zu:
balance
Das ist ein Grund, warum Schließungen in JavaScript so wichtig sind. Sie dienen nicht nur dazu, Variablen zu verbergen, sondern auch dazu, den Zustand über mehrere Aufrufe hinweg beizubehalten.
Korrektur einer ersten Annahme
Eine verlockende Zusammenfassung wäre, dass Schließungen das verborgene Laufzeitmechanismus liefern, während private und # lediglich Laufzeitwarnungen geben. Das ist nahe dran, aber falsch. private in TypeScript dient hauptsächlich zur Überprüfung des Zugriffs zur Kompilierzeit, während #private in JavaScript die Privatsphäre zur Laufzeit durchsetzt. Schließungen sind weder ein weiteres Schlüsselwort noch rein zur Kompilierzeit bestimmt. Sie ergeben sich aus dem lexikalischen Scoping, das Funktionen den Zugriff auf Variablen ihrer Umgebung ermöglicht. Dadurch kann Encapsulation realisiert werden, doch Schließungen umfassen weitaus mehr als nur Privatsphäre. Eine Funktionsfabrik zeigt einen Einsatzfall ohne Aspekte der Privatsphäre:
function createMultiplier(x) {
return function (y) {
return x * y;
};
}
const double = createMultiplier(2);console.log(double(10));
Hier merkt sich die zurückgegebene Funktion x, und das ist eine Schließung. Derselbe Mechanismus steuert private Zustände, Factory-Funktionen, Callbacks, Ereignishandler, Memoisierung und vieles mehr.
Ein mentales Modell der drei
Für TypeScript private:
class User {
private secret = "hidden";
}
Verstehen Sie es als das Typsystem, das verhindert, dass Sie auf das Member zugreifen. Für JavaScript #private:
class User {
#secret = "hidden";
}
Verstehen Sie das als den Laufzeitumgebungen, die das Feld tatsächlich als privat behandeln. Bei einer Schließung:
function createUser() {
let secret = "hidden";
return {
getSecret() {
return secret;
}
};
}
Betrachten Sie es als eine Funktion, die Variablen aus dem Scope, in dem sie erstellt wurde, speichert. Dies ist der entscheidende Punkt. Schließungen sind nicht veraltet, seit # existiert, denn #private ist eine Funktion für private Klassenfelder konzipiert, während Schließungen eine Folge des lexikalischen Scopings und erster-Klasse-Funktionen sind und vielen Zwecken dienen, die nichts mit Privatsphäre zu tun haben.
Hinter den Kulissen
Ein subtiler Punkt bleibt bestehen. Es wäre falsch zu behaupten, dass sowohl private als auch # letztendlich zu gewöhnlichen JavaScript-Eigenschaften werden. Das gilt für private in TypeScript im üblichen Ausgabeformat, aber nicht für die nativen #private-Felder:
class User {
#secret = "hidden";
}
Dies nutzt eine Laufzeitfunktion. Je nach Kompilierziel kann TypeScript die Syntax beibehalten oder sie für älteres JavaScript herabstufen, doch die Sprachsemantik erfordert weiterhin ein Verhalten ähnlich wie bei privaten Feldern. Legen Sie daher private und #private nicht in dieselbe Kategorie: Sie sehen in TypeScript ähnlich aus, werden aber unterschiedlich durchgesetzt, und Schließungen sind ein völlig anderes Mechanismus.
Sind Schließungen dann noch notwendig?
Ja, und eine bessere Frage als „Warum brauchen wir Closures trotz private und #?“ ist, welches Problem jedes dieser Mechanismen löst. Jeder beantwortet einen anderen Bedarf: private beschränkt den Zugriff innerhalb des Typprüfers, #private sorgt dafür, dass der Laufzeitumgebung selbst versteckte Felder durchgesetzt werden, und Closures ermöglichen es Funktionen, weiterhin den Kontext zu nutzen, in dem sie erstellt wurden. Wenn das klar ist, wirken Closures nicht mehr wie eine alte Workaround-Lösung für private Variablen, sondern zeigen ihre wahre Rolle als grundlegender Baustein des Programmiermodells von JavaScript. Das Erlernen neuer Funktionen macht ein älteres Konzept nicht irrelevant – es gibt vielmehr eine präzisere Frage dazu, nämlich warum der Mechanismus überhaupt nützlich war. Bei Closures geht die Antwort weit über das Verstecken einer Variablen hinaus.