Успадкування приватних полів
Приватні поля в JavaScript не успадковуються: #field прив'язане до тіла того класу, де його оголошено, тому підклас не може написати this.#field. Такий вираз, це SyntaxError ще на етапі парсингу, навіть якщо об'єкт фізично містить це поле, бо його створив конструктор батька.
Теорія
TL;DR
#fieldналежить одному конкретному тілу класу, а не прототипу, тому підклас його не бачить.- Звернення до
this.#parentFieldусередині нащадка, цеSyntaxError, а неundefined. - Однакові імена
#secretу батька і в нащадка, це два різні незалежні поля. - Публічні методи батька, що читають своє приватне поле, успадковуються і чудово працюють на екземплярі нащадка.
- Щоб поділитися даними з нащадками, використовують getter або метод, домовленість
_fieldчиWeakMap.
Швидкий приклад
class Parent {
#secret = 'parent secret';
showSecret() {
console.log(this.#secret);
}
}
class Child extends Parent {
reveal() {
// console.log(this.#secret);
// SyntaxError: Private field '#secret' must be declared in an enclosing class
}
}
const c = new Child();
c.showSecret(); // 'parent secret', метод батька бачить своє приватне полеЕкземпляр Child справді має поле #secret усередині, бо його встановив конструктор Parent. Але код класу Child не має права назвати це ім'я, тому рушій навіть не компілює такий модуль.
Як це працює під капотом
Коли ви оголошуєте #field, JavaScript створює унікальний прихований ідентифікатор, доступний лише всередині тіла цього класу. Приватне ім'я, це не рядок і не Symbol у звичайному сенсі: воно резолвиться лексично, як змінна, а не через властивість об'єкта.
Наслідок: однакові імена в різних класах, це різні поля без жодного зв'язку між собою.
class Parent {
#secret = 'parent';
getParentSecret() { return this.#secret; }
}
class Child extends Parent {
#secret = 'child';
getChildSecret() { return this.#secret; }
}
const c = new Child();
console.log(c.getParentSecret()); // 'parent'
console.log(c.getChildSecret()); // 'child'Один об'єкт спокійно тримає два поля з ім'ям #secret: одне «ключем» класу Parent, друге «ключем» класу Child. Жодного затінення чи конфлікту не відбувається.
Чому мова заборонила таке успадкування
Це свідоме рішення авторів специфікації:
- Сувора інкапсуляція. Усе, що починається з
#, приховане повністю, і його не можна відкрити ззовні через створення підкласу. - Відсутність конфліктів імен. Автор бібліотеки може додати
#cacheу базовий клас і не зламати нащадка, який теж має#cache. - Простіша оптимізація в рушіях. V8, SpiderMonkey та JavaScriptCore знають повний набір приватних імен класу під час компіляції і можуть розкласти їх у фіксовані слоти.
Тому extends успадковує методи та публічні поля, але не дає доступу до приватних полів батька.
Як поділитися даними з нащадками
Справжнього модифікатора protected у JavaScript немає, тому використовують один з трьох підходів.
Варіант 1: домовленість _field. Це не приватність, а угода «не чіпай ззовні», зате поле звичайне і повністю доступне нащадкам.
class Parent {
_secret = 'available to subclasses';
showSecret() {
console.log(this._secret);
}
}
class Child extends Parent {
reveal() {
console.log(this._secret); // працює
}
}Варіант 2: getter або метод батька. Приватність зберігається, а нащадок працює через інтерфейс і не знає деталей реалізації.
class Parent {
#secret = 'parent secret';
getSecret() {
return this.#secret;
}
}
class Child extends Parent {
reveal() {
console.log(this.getSecret()); // 'parent secret'
}
}
new Child().reveal();Варіант 3: WeakMap. Класичний спосіб приватності до ES2022. Дані лежать поза об'єктом, у замиканні модуля, тому ззовні недоступні, але батько може відкрити їх нащадкам через захищений аксесор.
const privates = new WeakMap();
class Parent {
constructor() {
privates.set(this, { secret: 'shared secret' });
}
get _data() {
return privates.get(this);
}
}
class Child extends Parent {
reveal() {
console.log(this._data.secret); // 'shared secret'
}
}
new Child().reveal();WeakMap не тримає сильного посилання на ключ, тому запис зникає разом зі знищенням екземпляра і витоку пам'яті не буде.
Порівняння підходів
| Підхід | Приватність | Доступ нащадкам | Реальний захист |
|---|---|---|---|
#field | повна | ні | так, на рівні синтаксису |
_field (домовленість) | часткова | так | ні, лише угода |
WeakMap | майже повна | так | так, дані поза об'єктом |
| getter або метод | через інтерфейс | так | так, контрольовано |
Типові помилки
- Очікувати
undefinedзамість помилки. Звернення до чужого#field, цеSyntaxErrorна етапі парсингу, тому падає весь модуль, а не один виклик. - Вважати, що однакове ім'я означає одне поле.
#secretу батька і#secretу нащадка, це два незалежні слоти; перевизначити батьківське поле в такий спосіб неможливо. - Думати, що методи теж не успадковуються. Публічні методи батька успадковуються нормально і всередині себе вільно читають власне приватне поле.
- Шукати
protectedу JavaScript. Такого модифікатора немає;protectedу TypeScript, це перевірка часу компіляції, у зібраному JS поле лишається публічним. - Оголошувати приватне поле лише в конструкторі нащадка. Приватне ім'я має бути оголошене в тілі класу, інакше присвоєння
this.#fieldтеж буде синтаксичною помилкою. - Плутати
#fieldз приватністю черезWeakMap.#fieldнедоступне навіть у підкласі, аWeakMapвіддає дані будь-кому, хто має посилання на саму мапу.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.