Jakie są różnice między prywatnym słowem kluczowym a prywatnymi polami w TypeScript?

27

W TypeScript 3.8+ jakie są różnice między użyciem privatesłowa kluczowego do oznaczenia członka jako prywatny:

class PrivateKeywordClass {
    private value = 1;
}

I używając #prywatnych pól proponowanych dla JavaScript :

class PrivateFieldClass {
    #value = 1;
}

Czy powinienem preferować jeden od drugiego?

Matt Bierner
źródło

Odpowiedzi:

43

Prywatne słowo kluczowe

Prywatny kluczowe w maszynopisie jest czas kompilacji adnotacji. Informuje kompilator, że właściwość powinna być dostępna tylko wewnątrz tej klasy:

class PrivateKeywordClass {
    private value = 1;
}

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

Sprawdzanie czasu kompilacji można jednak łatwo ominąć, na przykład odrzucając informacje o typie:

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

Słowo privatekluczowe nie jest również wymuszane w czasie wykonywania

Emitowany JavaScript

Podczas kompilowania TypeScript do JavaScript privatesłowo kluczowe jest po prostu usuwane:

class PrivateKeywordClass {
    private value = 1;
}

Staje się:

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

Na podstawie tego możesz zobaczyć, dlaczego privatesłowo kluczowe nie zapewnia żadnej ochrony środowiska wykonawczego: w wygenerowanym JavaScript jest to zwykła właściwość JavaScript.

Pola prywatne

Pola prywatne zapewniają, że właściwości są prywatne w czasie wykonywania :

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 wyświetli również błąd czasu kompilacji, jeśli spróbujesz użyć prywatnego pola poza klasą:

Błąd podczas uzyskiwania dostępu do prywatnego pola

Pola prywatne pochodzą z propozycji JavaScript, a także działają w normalnym JavaScript.

Emitowany JavaScript

Jeśli używasz pól prywatnych w TypeScript i celujesz w starsze wersje JavaScript dla swoich danych wyjściowych, takich jak es6lub es2018, TypeScript spróbuje wygenerować kod, który emuluje zachowanie w czasie wykonywania pól prywatnych

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

Jeśli esnextcelujesz, TypeScript wyśle ​​prywatne pole:

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

Którego powinienem użyć?

To zależy od tego, co próbujesz osiągnąć.

Słowo privatekluczowe jest w porządku. Osiąga to, do czego został zaprojektowany i od lat jest z powodzeniem wykorzystywany przez programistów TypeScript. A jeśli masz już bazę kodu, nie musisz przełączać całego kodu, aby korzystać z pól prywatnych. Jest to szczególnie prawdziwe, jeśli nie jesteś celem esnext, ponieważ JS emitowany przez TS dla pól prywatnych może mieć wpływ na wydajność. Należy również pamiętać, że pola prywatne mają inne subtelne, ale ważne różnice od privatesłowa kluczowego

Jeśli jednak chcesz wymusić prywatność środowiska wykonawczego lub wyprowadzasz esnextJavaScript, powinieneś użyć pól prywatnych.

Należy również pamiętać, że konwencje organizacji / społeczności dotyczące korzystania z jednego lub drugiego będą również ewoluować, gdy prywatne pola staną się bardziej rozpowszechnione w ekosystemach JavaScript / TypeScript

Inne różnice uwagi

  • Pola prywatne nie są zwracane za pomocą Object.getOwnPropertyNamespodobnych metod

  • Pola prywatne nie są serializowane przez JSON.stringify

  • Istnieją ważne przypadki brzegowe wokół dziedziczenia.

    Na przykład TypeScript zabrania deklarowania własności prywatnej w podklasie o takiej samej nazwie jak własność prywatna w nadklasie.

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

    Nie dotyczy to pól prywatnych:

    class Base {
        #value = 1;
    }
    
    class Sub extends Base {
        #value = 2; // Not an error
    }
  • Słowo privatekluczowe własność prywatna bez inicjatora nie wygeneruje deklaracji właściwości w emitowanym JavaScript:

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

    Kompiluje do:

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

    Podczas gdy pola prywatne zawsze generują deklarację właściwości:

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

    Kompiluje do (podczas targetowania esnext):

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

Dalsza lektura:

Matt Bierner
źródło
4

Przypadki użycia: #-prywatne pola

Przedmowa:

Prywatność w czasie kompilacji i wykonywania

#Pola prywatna zapewnić kompilacji i prywatność run-time, która nie jest „hackable”. Jest to mechanizm, aby uniemożliwić dostęp do członka z zewnątrz ciała klasy w żaden bezpośredni sposób .

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.

Bezpieczne dziedziczenie klas

#-prywatne pola mają unikalny zasięg. Hierarchie klas mogą być implementowane bez przypadkowego nadpisywania własności prywatnych o takich samych nazwach.

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"

Na szczęście kompilator TS emituje błąd, gdy privateistnieje ryzyko , że właściwości zostaną zastąpione (patrz ten przykład ). Ale ze względu na naturę funkcji czasu kompilacji wszystko jest nadal możliwe w czasie wykonywania, biorąc pod uwagę błędy kompilacji są ignorowane i / lub wykorzystywany jest kod JS.

Biblioteki zewnętrzne

Autorzy bibliotek mogą refaktoryzować #-prywatne identyfikatory bez powodowania przełomowej zmiany dla klientów. Użytkownicy biblioteki z drugiej strony są chronieni przed dostępem do pól wewnętrznych.

Interfejs API JS pomija #pola -prywatne

Wbudowane funkcje i metody JS ignorują #pola -prywatne. Może to skutkować bardziej przewidywalnym wyborem właściwości w czasie wykonywania. Przykłady: Object.keys, Object.entries, JSON.stringify, for..inpętla i inni ( przykładowy kod ; patrz także Matt Bierner za odpowiedź ):

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

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

Przypadki użycia: privatesłowo kluczowe

Przedmowa:

Dostęp do interfejsu API klasy wewnętrznej i stanu (prywatność tylko podczas kompilacji)

privateczłonkowie klasy są właściwościami konwencjonalnymi w czasie wykonywania. Możemy wykorzystać tę elastyczność, aby uzyskać dostęp do wewnętrznego interfejsu API klasy lub stanu z zewnątrz. Aby spełnić wymagania kompilatora, mechanizmy takie jak asercje typu, dynamiczny dostęp do właściwości lub @ts-ignoremogą być używane między innymi.

Przykład z asercją typu ( as/ <>) i anyprzypisaniem zmiennej typu:

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 pozwala nawet na dynamiczny dostęp do właściwości privateelementu z klapą ewakuacyjną :

class C {
  private foo = 10;
}

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

Gdzie prywatny dostęp może mieć sens? (1) testy jednostkowe, (2) sytuacje debugowania / rejestrowania lub (3) inne zaawansowane scenariusze przypadków z klasami wewnętrznymi projektu (lista otwarta).

Dostęp do zmiennych wewnętrznych jest trochę sprzeczny - inaczej nie stworzyłbyś ich privatew pierwszej kolejności. Na przykład testy jednostkowe powinny być czarnymi / szarymi skrzynkami z prywatnymi polami ukrytymi jako szczegóły implementacji. W praktyce jednak mogą istnieć odpowiednie podejścia w poszczególnych przypadkach.

Dostępne we wszystkich środowiskach ES

privateModyfikatory TS można stosować ze wszystkimi celami ES. #-prywatne pola są dostępne tylko dla target ES2015/ ES6lub wyższej. W ES6 + WeakMapjest wykorzystywany wewnętrznie jako implementacja niższego poziomu (patrz tutaj ). Natywne #pola prywatne obecnie wymagają target esnext.

Spójność i kompatybilność

Zespoły mogą korzystać ze wskazówek dotyczących kodowania i reguł linijek, aby wymusić użycie privatejako jedynego modyfikatora dostępu. To ograniczenie może pomóc zachować spójność i uniknąć pomylenia z #notacją pola -prywatnego w sposób zgodny z poprzednimi wersjami.

W razie potrzeby właściwości parametru (skrót konstruktora) są ogranicznikiem pokazu. Można ich używać tylko ze privatesłowem kluczowym i nie ma jeszcze planów ich wdrożenia dla #pól -prywatnych.

Inne powody

  • privatemoże zapewnić lepszą wydajność w czasie wykonywania w niektórych przypadkach obniżania poziomu (patrz tutaj ).
  • Do tej pory nie ma twardych metod klasy prywatnej w TS.
  • Niektórym bardziej podoba się privatenotacja słowa kluczowego 😊.

Uwaga na oba

Oba podejścia tworzą rodzaj nominalnego lub markowego typu w czasie kompilacji.

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

Oba umożliwiają także dostęp między instancjami: instancja klasy Amoże uzyskać dostęp do prywatnych członków innych Ainstancji:

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

Źródła

ford04
źródło