Каковы различия между частным ключевым словом и частными полями в TypeScript?

27

В TypeScript 3.8+, каковы различия между использованием privateключевого слова для пометки члена как частного:

class PrivateKeywordClass {
    private value = 1;
}

И используя #частные поля, предлагаемые для JavaScript :

class PrivateFieldClass {
    #value = 1;
}

Должен ли я предпочесть один другому?

Мэтт Бирнер
источник

Ответы:

43

Приватное ключевое слово

Частное ключевым словом в машинописном является компиляцией времени аннотаций. Он сообщает компилятору, что свойство должно быть доступно только внутри этого класса:

class PrivateKeywordClass {
    private value = 1;
}

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

Однако проверку времени компиляции можно легко обойти, например, отбрасывая информацию о типе:

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

privateКлючевое слово также не соблюдается во время выполнения

Испускаемый JavaScript

При компиляции TypeScript в JavaScript privateключевое слово просто удаляется:

class PrivateKeywordClass {
    private value = 1;
}

становится:

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

Отсюда видно, почему privateключевое слово не обеспечивает никакой защиты во время выполнения: в сгенерированном JavaScript это просто обычное свойство JavaScript.

Частные поля

Частные поля гарантируют, что свойства остаются частными во время выполнения :

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 также выведет ошибку времени компиляции, если вы попытаетесь использовать приватное поле вне класса:

Ошибка при доступе к приватному полю

Закрытые поля приходят из предложения JavaScript, а также работают в обычном JavaScript.

Испускаемый JavaScript

Если вы используете закрытые поля в TypeScript и нацеливаетесь на более ранние версии JavaScript для вывода, например, es6или es2018, TypeScript попытается сгенерировать код, который имитирует поведение закрытых полей во время выполнения

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

Если вы нацеливаетесь esnext, TypeScript выдаст приватное поле:

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

Какой из них я должен использовать?

Это зависит от того, чего вы пытаетесь достичь.

privateКлючевым словом является штраф по умолчанию. Он выполняет то, для чего был разработан, и успешно используется разработчиками TypeScript в течение многих лет. И если у вас есть существующая кодовая база, вам не нужно переключать весь код на использование приватных полей. Это особенно верно, если вы не нацеливаетесь esnext, поскольку JS, который TS генерирует для частных полей, может повлиять на производительность. Также имейте в виду, что частные поля имеют другие тонкие, но важные отличия от privateключевого слова.

Однако, если вам нужно обеспечить конфиденциальность во время выполнения или вывести esnextJavaScript, вам следует использовать закрытые поля.

Также имейте в виду, что соглашения организации / сообщества об использовании одного или другого также будут развиваться по мере того, как частные поля станут более распространенными в экосистемах JavaScript / TypeScript.

Другие отличия примечания

  • Закрытые поля не возвращаются Object.getOwnPropertyNamesи аналогичными методами

  • Частные поля не сериализуются JSON.stringify

  • Существуют крайние случаи важности вокруг наследования.

    Например, TypeScript запрещает объявлять частное свойство в подклассе с тем же именем, что и частное свойство в суперклассе.

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

    Это не так с частными полями:

    class Base {
        #value = 1;
    }
    
    class Sub extends Base {
        #value = 2; // Not an error
    }
  • privateКлючевое слово частной собственности без инициализатора не сгенерирует объявление свойства в излучаемом JavaScript:

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

    Компилируется в:

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

    Принимая во внимание, что частные поля всегда генерируют объявление свойства:

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

    Компилируется в (при таргетинге esnext):

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

Дальнейшее чтение:

Мэтт Бирнер
источник
4

#Варианты использования: -приватные поля

Предисловие:

Конфиденциальность во время компиляции и во время выполнения

#Поля -private обеспечивают конфиденциальность во время компиляции и во время выполнения, что не является "взломанным". Это механизм, предотвращающий доступ к члену вне тела класса любым прямым способом .

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.

Безопасное наследование классов

#-приватные поля получают уникальную область видимости. Иерархии классов могут быть реализованы без случайной перезаписи частных свойств с одинаковыми именами.

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"

Компилятор TS, к счастью, выдает ошибку, когда privateсвойства могут быть перезаписаны (см. Этот пример ). Но из-за особенностей функции времени компиляции все еще возможно во время выполнения, учитывая, что ошибки компиляции игнорируются и / или испускается код JS.

Внешние библиотеки

Авторы библиотеки могут рефакторировать #-приватные идентификаторы, не вызывая серьезных изменений для клиентов. Пользователи библиотеки с другой стороны защищены от доступа к внутренним полям.

JS API опускает #-приватные поля

Встроенные функции и методы JS игнорируют #поля -private. Это может привести к более предсказуемому выбору свойств во время выполнения. Примеры: Object.keys, Object.entries, JSON.stringify, for..inпетли и другие ( пример кода , см также Мэтта Bierner в ответ ):

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

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

Варианты использования: privateключевое слово

Предисловие:

Доступ к внутреннему API класса и состоянию (конфиденциальность только во время компиляции)

privateчлены класса являются обычными свойствами во время выполнения. Мы можем использовать эту гибкость для доступа к внутреннему API-интерфейсу класса или состоянию извне. Для выполнения проверок компилятором @ts-ignoreмогут использоваться такие механизмы, как утверждения типа, динамический доступ к свойствам или другие.

Пример с утверждением типа ( as/ <>) и anyтипизированной переменной:

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 даже позволяет динамический доступ к свойству privateчлена с помощью escape-люка :

class C {
  private foo = 10;
}

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

Где может иметь смысл частный доступ? (1) модульные тесты, (2) ситуации отладки / регистрации или (3) другие сложные сценарии с внутренними классами проекта (открытый список).

Доступ к внутренним переменным немного противоречив, иначе вы бы не сделали их privateв первую очередь. В качестве примера, модульные тесты должны быть черными / серыми блоками с закрытыми полями, скрытыми как детали реализации. На практике, тем не менее, могут существовать подходы от случая к случаю.

Доступно во всех средах ES

privateМодификаторы TS могут использоваться со всеми целями ES. #-приватные поля доступны только для target ES2015/ ES6или выше. В ES6 + WeakMapиспользуется внутри как реализация нижнего уровня (см. Здесь ). Собственные #-приватные поля в настоящее время требуют target esnext.

Согласованность и совместимость

Команды могут использовать рекомендации по кодированию и правилам линтера, чтобы обеспечить использование в privateкачестве единственного модификатора доступа. Это ограничение может помочь в согласованности и избежать путаницы с #нотацией -private поля обратно совместимым образом.

При необходимости свойства параметров (сокращение назначения конструктора) являются ограничителем показа. Их можно использовать только с privateключевым словом, и пока нет планов по их реализации для #полей -private.

Другие причины

  • privateможет обеспечить лучшую производительность во время выполнения в некоторых случаях понижения (см. здесь ).
  • До сих пор в TS нет жестких методов закрытого класса.
  • Некоторым людям нравится privateнотация ключевых слов 😊.

Обратите внимание на оба

Оба подхода создают некий номинальный или фирменный тип во время компиляции.

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

Кроме того, оба разрешают доступ между экземплярами: экземпляр класса Aможет обращаться к закрытым членам других Aэкземпляров:

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

источники

ford04
источник