Wir alle wollten vor langer Zeit eine normale Kapselung in JS, die ohne unnötige Gesten verwendet werden kann. Wir wollen auch bequeme Konstruktionen zum Deklarieren von Klasseneigenschaften. Und schließlich möchten wir, dass all diese Funktionen in der Sprache so angezeigt werden, dass vorhandene Anwendungen nicht beschädigt werden.
Es scheint, dass es hier Glück ist: Klassenfeld-Vorschlag , der nach vielen Jahren der Qual des tc39-Komitees noch stage 3
und sogar in Chrom umgesetzt wurde .
Ehrlich gesagt würde ich wirklich gerne einen Artikel darüber schreiben, warum Sie die neue Sprachfunktion verwenden sollten und wie es geht, aber leider wird der Artikel überhaupt nicht darüber handeln.
Beschreibung des fehlenden Stroms
Ich werde die ursprüngliche Beschreibung , FAQ und Änderungen in den Spezifikationen hier nicht wiederholen, sondern nur kurz die wichtigsten Punkte skizzieren.
Klassenfelder
Felder deklarieren und innerhalb einer Klasse verwenden:
class A { x = 1; method() { console.log(this.x); } }
Zugriff auf Felder außerhalb der Klasse:
const a = new A(); console.log(ax);
Alles schien offensichtlich zu sein und seit vielen Jahren verwenden wir diese Syntax mit Babel und TypeScript .
Nur gibt es eine Nuance. Diese neue Syntax verwendet [[Define]]
und nicht [[Set]]
Semantik, mit der wir die ganze Zeit gelebt haben.
In der Praxis bedeutet dies, dass der obige Code nicht dem folgenden entspricht :
class A { constructor() { this.x = 1; } method() { console.log(this.x); } }
Aber eigentlich ist es gleichbedeutend damit:
class A { constructor() { Object.defineProperty(this, "x", { configurable: true, enumerable: true, writable: true, value: 1 }); } method() { console.log(this.x); } }
Und obwohl für das obige Beispiel beide Ansätze im Wesentlichen dasselbe tun, ist dies ein SEHR ERNSTER Unterschied, und hier ist der Grund:
Nehmen wir an, wir haben eine Elternklasse wie diese:
class A { x = 1; method() { console.log(this.x); } }
Darauf basierend haben wir eine weitere erstellt:
class B extends A { x = 2; }
Und sie haben es benutzt:
const b = new B(); b.method();
Dann wurde aus irgendeinem Grund die Klasse A
scheinbar abwärtskompatibel geändert:
class A { _x = 1;
Und für die [[Set]]
Semantik ist dies wirklich eine abwärtskompatible Änderung, jedoch nicht für [[Define]]
. Jetzt wird der Aufruf von b.method()
an Konsole 1
anstatt an 2
ausgegeben. Dies geschieht, weil Object.defineProperty
den Eigenschaftsdeskriptor neu definiert und dementsprechend Getter / Setter aus Klasse A
nicht aufgerufen wird. Tatsächlich haben wir in der untergeordneten Klasse die x
Eigenschaft des Elternteils verdeckt , ähnlich wie wir dies im lexikalischen Bereich tun können:
const x = 1; { const x = 2; }
In diesem Fall wird uns zwar der Linter mit seinen Regeln für no-shadowed-variable
/ no-shadow
retten, aber die Wahrscheinlichkeit, dass jemand ein no-shadowed-class-field
für no-shadowed-class-field
gegen Null.
Übrigens werde ich für die erfolgreichere russische Bezeichnung für shadowed
dankbar sein.
Trotz alledem bin ich kein inakzeptabler Gegner der neuen Semantik (obwohl ich eine andere bevorzugen würde), weil sie ihre eigenen positiven Aspekte hat. Leider überwiegen diese Pluspunkte nicht das wichtigste Minus - wir verwenden seit vielen Jahren die Semantik [[Set]]
, da sie standardmäßig in babel6
und TypeScript
wird.
Es babel7
, dass in babel7
Standardwert geändert wurde .
Weitere originelle Diskussionen zu diesem Thema finden Sie hier und hier .
Private Felder
Und jetzt kommen wir zum umstrittensten Teil dieses Artikels. So umstritten, dass:
- Trotz der Tatsache, dass es bereits in Chrome Canary implementiert ist und öffentliche Felder bereits standardmäßig aktiviert sind, befinden sich private Felder immer noch hinter der Flagge.
- Trotz der Tatsache, dass das ursprüngliche Prozal für private Felder mit dem aktuellen zusammengeführt wurde, werden immer noch Anforderungen für die Trennung dieser beiden Merkmale (z. B. eins , zwei , drei und vier ) erstellt.
- Sogar einige Komiteemitglieder (wie Allen Wirfs-Brock und Kevin Smith ) sprechen sich aus und bieten trotz Stufe 3 Alternativen an .
- Dieser verpasste Rekord stellte einen Rekord für die Anzahl der Ausgaben auf - 129 im aktuellen Repository + 96 im Original gegenüber 126 für BigInt , und der Rekordhalter hat überwiegend negative Kommentare .
- Ich musste einen separaten Thread erstellen, um alle Ansprüche dagegen irgendwie zusammenzufassen.
- Ich musste eine separate FAQ schreiben, die diesen Teil abdeckt
Aufgrund einer eher schwachen Argumentation erschienen solche Diskussionen jedoch ( eins , zwei ).
- Ich persönlich verbrachte meine ganze Freizeit (und manchmal auch meine Arbeit) über einen langen Zeitraum, um alles herauszufinden und sogar eine Erklärung dafür zu finden, warum er so war, oder eine geeignete Alternative anzubieten.
- Am Ende habe ich beschlossen, diesen Übersichtsartikel zu schreiben.
Private Felder werden wie folgt deklariert:
class A { #priv; }
Der Zugang zu ihnen ist wie folgt:
class A { #priv = 1; method() { console.log(this.#priv); } }
Ich werde nicht einmal das Thema ansprechen, dass das mentale Modell dahinter nicht sehr intuitiv ist ( this.#priv !== this['#priv']
), nicht die bereits reservierten private
/ protected
Wörter verwendet (was notwendigerweise zusätzlichen Schmerz verursacht für TypeScript-Entwickler) ist nicht klar, wie es für andere Zugriffsmodifikatoren erweitert werden soll , und die Syntax selbst ist nicht sehr schön. Obwohl all dies der ursprüngliche Grund war, der mich zu einem tieferen Studium und zur Teilnahme an Diskussionen veranlasste.
Dies alles bezieht sich auf die Syntax, bei der die subjektiven ästhetischen Präferenzen sehr stark sind. Und man könnte damit leben und sich mit der Zeit daran gewöhnen. Wenn nicht für eine Sache: Es gibt ein sehr bedeutendes Problem der Semantik ...
Semantik WeakMap
Werfen wir einen Blick darauf, was hinter dem bestehenden Vorschlag steckt. Wir können das obige Beispiel mit Kapselung und ohne Verwendung der neuen Syntax umschreiben, wobei jedoch die Semantik der aktuellen beibehalten wird:
const privatesForA = new WeakMap(); class A { constructor() { privatesForA.set(this, {}); privatesForA.get(this).priv = 1; } method() { console.log(privatesForA.get(this).priv); } }
Auf der Grundlage dieser Semantik hat eines der Komiteemitglieder übrigens sogar eine kleine Utility-Bibliothek erstellt , mit der Sie den privaten Staat jetzt nutzen können, um zu zeigen, dass diese Funktionalität vom Komitee zu überbewertet wird. Formatierter Code benötigt nur 27 Zeilen.
Im Allgemeinen ist alles ziemlich gut, wir werden hard-private
, was in keiner Weise von externem Code abgerufen / abgefangen / verfolgt werden kann, und gleichzeitig können wir auf die privaten Felder einer anderen Instanz derselben Klasse zugreifen, zum Beispiel wie folgt:
isEquals(obj) { return privatesForA.get(this).id === privatesForA.get(obj).id; }
Nun, dies ist sehr praktisch, mit der Ausnahme, dass diese Semantik neben der Kapselung selbst auch die brand-checking
umfasst (Sie können nicht googeln, was es ist - es ist unwahrscheinlich, dass Sie relevante Informationen finden).
brand-checking
ist das Gegenteil von duck-typing
in dem Sinne, dass nicht die öffentliche Schnittstelle des Objekts überprüft wird, sondern die Tatsache, dass das Objekt mit vertrauenswürdigem Code erstellt wurde.
Eine solche Prüfung hat in der Tat einen gewissen Umfang - sie hängt hauptsächlich mit der Sicherheit zusammen, nicht vertrauenswürdigen Code in einem einzigen Adressraum mit einem vertrauenswürdigen aufzurufen, und mit der Fähigkeit, Objekte ohne Serialisierung direkt auszutauschen.
Obwohl einige Ingenieure dies als einen notwendigen Teil der ordnungsgemäßen Kapselung betrachten.
Trotz der Tatsache, dass dies nach meiner Erfahrung eine ziemlich merkwürdige Gelegenheit ist, die eng mit dem
( kurze und längere Beschreibung), der Realms
und der wissenschaftlichen Arbeit auf dem Gebiet der Informatik zusammenhängt, mit der sich Mark Samuel Miller beschäftigt (er ist auch Mitglied des Komitees) In der Praxis der meisten Entwickler tritt dies fast nie auf.
Übrigens bin ich immer noch auf eine Membran gestoßen (obwohl ich damals nicht wusste, was es war), als ich vm2 neu geschrieben habe, um meinen Anforderungen zu entsprechen.
Problem bei der brand-checking
Wie bereits erwähnt, ist die brand-checking
das Gegenteil von duck-typing
. In der Praxis bedeutet dies, dass Sie diesen Code haben:
const brands = new WeakMap(); class A { constructor() { brands.set(this, {}); } method() { return 1; } brandCheckedMethod() { if (!brands.has(this)) throw 'Brand-check failed'; console.log(this.method()); } }
brandCheckedMethod
kann nur mit einer Instanz der Klasse A
aufgerufen werden. Selbst wenn das Ziel ein Objekt ist, das die Invarianten dieser Klasse brandCheckedMethod
diese Methode eine Ausnahme aus:
const duckTypedObj = { method: A.prototype.method.bind(duckTypedObj), brandCheckedMethod: A.prototype.brandCheckedMethod.bind(duckTypedObj), }; duckTypedObj.method();
Offensichtlich ist dieses Beispiel ziemlich synthetisch und die Verwendung von duckTypedObj
wie diesem duckTypedObj
zweifelhaft, bis wir über Proxy
nachdenken.
Eines der sehr wichtigen Proxy-Nutzungsszenarien ist die Metaprogrammierung. Damit der Proxy alle erforderlichen nützlichen Arbeiten ausführen kann, müssen die Methoden von Objekten, die mit einem Proxy verpackt werden, im Kontext des Proxys und nicht im Kontext des Ziels ausgeführt werden, d. H.:
const a = new A(); const proxy = new Proxy(a, { get(target, p, receiver) { const property = Reflect.get(target, p, receiver); doSomethingUseful('get', retval, target, p, receiver); return (typeof property === 'function') ? property.bind(proxy) : property; } });
Rufen Sie proxy.method();
führt nützliche Arbeit aus, die im Proxy deklariert ist, und gibt 1
, während proxy.brandCheckedMethod();
Anstatt zweimal nützliche Arbeit vom Proxy aus zu erledigen, wird eine Ausnahme a !== proxy
, da a !== proxy
, was bedeutet, dass die brand-check
nicht bestanden wurde.
Ja, wir können Methoden / Funktionen im Kontext eines realen Ziels ausführen, nicht eines Proxys. In einigen Szenarien reicht dies aus (z. B. um das
zu implementieren), dies reicht jedoch nicht in allen Fällen aus (z. B. um reaktive Eigenschaften zu implementieren: MobX 5 verwendet bereits einen Proxy Aus diesem Grund experimentieren Vue.js und Aurelia mit diesem Ansatz für zukünftige Versionen.
Solange die brand-check
explizit durchgeführt werden muss, ist dies im Allgemeinen kein Problem. Der Entwickler muss nur bewusst entscheiden, welchen Kompromiss er eingeht und ob er sie benötigt. Darüber hinaus können Sie sie bei einer expliziten brand-check
so implementieren brand-check
Sie sie implementieren können Der Fehler wird nicht auf vertrauenswürdige Proxys übertragen.
Leider hat uns der aktuelle diese Flexibilität genommen:
class A { #priv; method() { this.#priv;
Eine solche method
löst immer eine Ausnahme aus, wenn sie nicht im Kontext eines Objekts aufgerufen wird, das mit dem Konstruktor A
Und das Schlimmste ist, dass die brand-check
hier implizit ist und mit einer anderen Funktionalität gemischt wird - der Kapselung.
Während die
für jeden Code fast notwendig ist, hat die brand-check
einen eher engen Umfang. Die Kombination zu einer Syntax führt dazu, dass im Benutzercode viele unbeabsichtigte brand-check
werden, wenn der Entwickler nur die Implementierungsdetails verbergen wollte.
Und der Slogan, der verwendet wird, um dies zu fördern, war # is the new _
die Situation nur verschärft.
Sie können auch eine ausführliche Diskussion darüber lesen, wie ein vorhandenes Prozal einen Proxy bricht . Einer der Aurelia-Entwickler und Autor Vue.js sprach in der Diskussion .
Auch mein Kommentar , der den Unterschied zwischen verschiedenen Proxy-Szenarien genauer beschreibt, mag für jemanden interessant erscheinen. Insgesamt die gesamte Diskussion über die Verbindung von privaten Feldern und Membranen .
Alternativen
All diese Diskussionen wären wenig sinnvoll, wenn es keine Alternativen gäbe. Leider hat kein einziger Stellvertreter die Stufe 1 erreicht und hatte daher nicht einmal die Chance, genug trainiert zu werden. Ich werde hier jedoch Alternativen auflisten, die die oben beschriebenen Probleme irgendwie lösen.
- Symbol.private - ein alternatives prozazil eines der Komiteemitglieder.
- Es löst alle oben genannten Probleme (obwohl es vielleicht seine eigenen hat, aber angesichts des Mangels an aktiver Arbeit daran ist es schwierig, sie zu finden)
- Bei der letzten Sitzung des Ausschusses wurde es aufgrund des Fehlens eines integrierten
brand-check
, der Probleme mit dem Membranmuster (obwohl dies eine angemessene Lösung darstellt) und des Mangels an praktischer Syntax erneut zurückgeworfen - Wie ich hier und hier gezeigt habe, kann eine praktische Syntax auf der tatsächlichen aufgebaut werden
- Klassen 1.1 - früheres Posozal desselben Autors
- Private als Objekt verwenden
Anstelle einer Schlussfolgerung
Nach dem Ton des Artikels scheint es wahrscheinlich, dass ich das Komitee verurteile - das ist nicht so. Es scheint mir nur, dass sich das Komitee im Laufe der Jahre (je nach Ausgangspunkt könnten es sogar Jahrzehnte sein) mit der Kapselung in JS befasst hat, viel in der Branche hat sich geändert, und das Aussehen könnte verschwommen sein, was zu einer falschen Rangfolge der Prioritäten geführt hat .
Darüber hinaus drängen wir als Community auf tc39 und zwingen sie, Features schneller zu veröffentlichen, während wir in den frühen Stadien von prozos äußerst wenig Feedback geben, was unsere Empörung nur zu einem Zeitpunkt verringert, an dem wenig geändert werden kann.
Es wird angenommen, dass in diesem Fall der Prozess einfach fehlgeschlagen ist.
Nachdem ich es mir in den Kopf gesetzt und mit einigen Vertretern gesprochen hatte, beschloss ich, mein Bestes zu geben, um ein Wiederauftreten einer ähnlichen Situation zu verhindern - aber ich kann ein wenig tun (einen Übersichtsartikel schreiben, die Implementierung von stage1
fehlte in babel
und das ist alles).
Das Wichtigste ist jedoch das Feedback. Ich möchte Sie daher bitten, an dieser kleinen Umfrage teilzunehmen. Und ich werde wiederum versuchen, es dem Ausschuss zu übermitteln.