Was sind die Unterschiede zwischen dem privaten Schlüsselwort und privaten Feldern in TypeScript?

27

Was sind in TypeScript 3.8+ die Unterschiede zwischen der Verwendung des privateSchlüsselworts zum Markieren eines privaten Mitglieds:

class PrivateKeywordClass {
    private value = 1;
}

Und unter Verwendung der für JavaScript vorgeschlagenen# privaten Felder :

class PrivateFieldClass {
    #value = 1;
}

Soll ich eins dem anderen vorziehen?

Matt Bierner
quelle

Antworten:

43

Privates Schlüsselwort

Das private Schlüsselwort in TypeScript ist eine Anmerkung zur Kompilierungszeit . Es teilt dem Compiler mit, dass auf eine Eigenschaft nur innerhalb dieser Klasse zugegriffen werden soll:

class PrivateKeywordClass {
    private value = 1;
}

const obj = new PrivateKeywordClass();
obj.value // compiler error: Property 'value' is private and only accessible within class 'PrivateKeywordClass'.

Die Überprüfung der Kompilierungszeit kann jedoch leicht umgangen werden, indem beispielsweise die Typinformationen weggeworfen werden:

const obj = new PrivateKeywordClass();
(obj as any).value // no compile error

Das privateSchlüsselwort wird zur Laufzeit auch nicht erzwungen

Ausgelassenes JavaScript

Beim Kompilieren von TypeScript zu JavaScript wird das privateSchlüsselwort einfach entfernt:

class PrivateKeywordClass {
    private value = 1;
}

Wird:

class PrivateKeywordClass {
    constructor() {
        this.value = 1;
    }
}

Daran können Sie erkennen, warum das privateSchlüsselwort keinen Laufzeitschutz bietet: Im generierten JavaScript ist es nur eine normale JavaScript-Eigenschaft.

Private Felder

Private Felder stellen sicher, dass Eigenschaften zur Laufzeit privat bleiben :

class PrivateFieldClass {
    #value = 1;

    getValue() { return this.#value; }
}

const obj = new PrivateFieldClass();

// You can't access '#value' outside of class like this
obj.value === undefined // This is not the field you are looking for.
obj.getValue() === 1 // But the class itself can access the private field!

// Meanwhile, using a private field outside a class is a runtime syntax error:
obj.#value

// While trying to access the private fields of another class is 
// a runtime type error:
class Other {
    #value;

    getValue(obj) {
        return obj.#value // TypeError: Read of private field #value from an object which did not contain the field
    }
}

new Other().getValue(new PrivateKeywordClass());

TypeScript gibt auch einen Fehler bei der Kompilierung aus, wenn Sie versuchen, ein privates Feld außerhalb einer Klasse zu verwenden:

Fehler beim Zugriff auf ein privates Feld

Private Felder stammen aus einem JavaScript-Vorschlag und funktionieren auch in normalem JavaScript.

Ausgelassenes JavaScript

Wenn Sie private Felder in TypeScript verwenden und ältere Versionen von JavaScript für Ihre Ausgabe verwenden, z. B. es6oder es2018, versucht TypeScript, Code zu generieren, der das Laufzeitverhalten privater Felder emuliert

class PrivateFieldClass {
    constructor() {
        _x.set(this, 1);
    }
}
_x = new WeakMap();

Wenn Sie ein Targeting durchführen esnext, gibt TypeScript das private Feld aus:

class PrivateFieldClass {
    constructor() {
        this.#x = 1;
    }
    #x;
}

Welches soll ich verwenden?

Es hängt davon ab, was Sie erreichen wollen.

Das privateSchlüsselwort ist eine gute Standardeinstellung. Es erreicht das, wofür es entwickelt wurde, und wird seit Jahren von TypeScript-Entwicklern erfolgreich eingesetzt. Wenn Sie über eine vorhandene Codebasis verfügen, müssen Sie nicht den gesamten Code wechseln, um private Felder zu verwenden. Dies gilt insbesondere dann, wenn Sie kein Targeting durchführen esnext, da die von TS für private Felder ausgegebene JS möglicherweise Auswirkungen auf die Leistung hat. Beachten Sie auch, dass private Felder andere subtile, aber wichtige Unterschiede zum privateSchlüsselwort aufweisen

Wenn Sie jedoch die Privatsphäre der Laufzeit erzwingen müssen oder esnextJavaScript ausgeben , sollten Sie private Felder verwenden.

Denken Sie auch daran, dass sich Organisations- / Community-Konventionen zur Verwendung des einen oder anderen ebenfalls ändern werden, wenn private Felder in den JavaScript / TypeScript-Ökosystemen weiter verbreitet werden

Andere bemerkenswerte Unterschiede

  • Private Felder werden von Object.getOwnPropertyNamesund ähnlichen Methoden nicht zurückgegeben

  • Private Felder werden von nicht serialisiert JSON.stringify

  • Es gibt wichtige Randfälle im Zusammenhang mit der Vererbung.

    TypeScript verbietet beispielsweise das Deklarieren einer privaten Eigenschaft in einer Unterklasse mit demselben Namen wie eine private Eigenschaft in der Oberklasse.

    class Base {
        private value = 1;
    }
    
    class Sub extends Base {
        private value = 2; // Compile error:
    }

    Dies gilt nicht für private Felder:

    class Base {
        #value = 1;
    }
    
    class Sub extends Base {
        #value = 2; // Not an error
    }
  • Ein privateSchlüsselwort private Eigenschaft ohne Initialisierer generiert keine Eigenschaftsdeklaration im ausgegebenen JavaScript:

    class PrivateKeywordClass {
        private value?: string;
        getValue() { return this.value; }
    }

    Kompiliert zu:

    class PrivateKeywordClass {
        getValue() { return this.value; }
    }

    Während private Felder immer eine Eigenschaftsdeklaration generieren:

    class PrivateKeywordClass {
        #value?: string;
        getValue() { return this.#value; }
    }

    Kompiliert zu (beim Zielen esnext):

    class PrivateKeywordClass {
        #value;
        getValue() { return this.#value; }
    }

Weiterführende Literatur:

Matt Bierner
quelle
4

Anwendungsfälle: # -private Felder

Vorwort:

Datenschutz zur Kompilierungs- und Laufzeit

#-private Felder bieten Datenschutz zur Kompilierungs- und Laufzeit, der nicht "hackbar" ist. Es ist ein Mechanismus, der den direkten Zugriff auf ein Mitglied von außerhalb des Klassenkörpers verhindert .

class A {
    #a: number;
    constructor(a: number) {
        this.#a = a;
    }
}

let foo: A = new A(42);
foo.#a; // error, not allowed outside class bodies
(foo as any).#bar; // still nope.

Sichere Klassenvererbung

#-private Felder erhalten einen eindeutigen Bereich. Klassenhierarchien können implementiert werden, ohne dass private Eigenschaften mit gleichen Namen versehentlich überschrieben werden.

class A { 
    #a = "a";
    fnA() { return this.#a; }
}

class B extends A {
    #a = "b"; 
    fnB() { return this.#a; }
}

const b = new B();
b.fnA(); // returns "a" ; unique property #a in A is still retained
b.fnB(); // returns "b"

Der TS-Compiler gibt glücklicherweise einen Fehler aus, wenn privatedie Gefahr besteht , dass Eigenschaften überschrieben werden (siehe dieses Beispiel) ). Aufgrund der Art einer Funktion zur Kompilierungszeit ist zur Laufzeit jedoch immer noch alles möglich, da Kompilierungsfehler ignoriert und / oder ausgegebener JS-Code verwendet wird.

Externe Bibliotheken

Bibliotheksautoren können #private Kennungen umgestalten, ohne dass dies zu einer grundlegenden Änderung für Clients führt. Bibliotheksbenutzer auf der anderen Seite sind vor dem Zugriff auf interne Felder geschützt.

In der JS-API werden #private Felder weggelassen

Integrierte JS-Funktionen und -Methoden ignorieren #-private Felder. Dies kann zur Laufzeit zu einer besser vorhersehbaren Eigenschaftsauswahl führen. Beispiele: Object.keys, Object.entries, JSON.stringify, for..inSchleife und andere ( Codebeispiel , siehe auch Matt Bierner Antwort ):

class Foo {
    #bar = 42;
    baz = "huhu";
}

Object.keys(new Foo()); // [ "baz" ]

Anwendungsfälle: privateSchlüsselwort

Vorwort:

Zugriff auf interne Klassen-API und -Status (nur Datenschutz zur Kompilierungszeit)

privateMitglieder einer Klasse sind zur Laufzeit herkömmliche Eigenschaften. Wir können diese Flexibilität nutzen, um von außen auf die klasseninterne API oder den Status zuzugreifen. Um Compilerprüfungen zu erfüllen, @ts-ignorekönnen unter anderem Mechanismen wie Typzusicherungen, dynamischer Eigenschaftszugriff oder verwendet werden.

Beispiel mit Typzusicherung ( as/ <>) und anytypisierter Variablenzuweisung:

class A { 
    constructor(private a: number) { }
}

const a = new A(10);
a.a; // TS compile error
(a as any).a; // works
const casted: any = a; casted.a // works

TS ermöglicht sogar den dynamischen Eigenschaftszugriff eines privateMitglieds mit einer Notluke :

class C {
  private foo = 10;
}

const res = new C()["foo"]; // 10, res has type number

Wo kann ein privater Zugang sinnvoll sein? (1) Unit-Tests, (2) Debugging- / Protokollierungssituationen oder (3) andere fortgeschrittene Fallszenarien mit projektinternen Klassen (offene Liste).

Der Zugriff auf interne Variablen ist etwas widersprüchlich - sonst hätten Sie sie gar nicht erst gemacht private. Bei Unit-Tests handelt es sich beispielsweise um schwarz / graue Kästchen mit privaten Feldern, die als Implementierungsdetail ausgeblendet sind. In der Praxis kann es jedoch von Fall zu Fall gültige Ansätze geben.

Verfügbar in allen ES-Umgebungen

TS- privateModifikatoren können mit allen ES-Zielen verwendet werden. #-private Felder sind nur für target ES2015/ ES6oder höher verfügbar . WeakMapWird in ES6 + intern als Downlevel-Implementierung verwendet (siehe hier ). Native- #private Felder sind derzeit erforderlich target esnext.

Konsistenz und Kompatibilität

Teams verwenden möglicherweise Codierungsrichtlinien und Linter-Regeln, um die Verwendung privateals einzigen Zugriffsmodifikator zu erzwingen . Diese Einschränkung kann zur Konsistenz beitragen und Verwechslungen mit der #privaten Feldnotation auf abwärtskompatible Weise vermeiden .

Bei Bedarf sind Parametereigenschaften (Konstruktorzuweisungskürzel) ein Show-Stopper. Sie können nur mit privateSchlüsselwörtern verwendet werden, und es gibt noch keine Pläne , sie für #private Felder zu implementieren .

Andere Gründe

  • privatekann in einigen Down-Leveling-Fällen eine bessere Laufzeitleistung bieten (siehe hier ).
  • Bisher sind in TS keine harten Methoden für Privatklassen verfügbar.
  • Einige Leute mögen die privateKeyword-Notation besser 😊.

Hinweis zu beiden

Beide Ansätze erzeugen zur Kompilierungszeit eine Art Nominal- oder Markentyp.

class A1 { private a = 0; }
class A2 { private a = 42; }

const a: A1 = new A2(); 
// error: "separate declarations of a private property 'a'"
// same with hard private fields

Außerdem ermöglichen beide den instanzübergreifenden Zugriff: Eine Klasseninstanz Akann auf private Mitglieder anderer AInstanzen zugreifen :

class A {
    private a = 0;
    method(arg: A) {
        console.log(arg.a); // works
    }
}

Quellen

ford04
quelle