Skip to main content

Успадкування приватних полів

Приватні поля в JavaScript не успадковуються: #field прив'язане до тіла того класу, де його оголошено, тому підклас не може написати this.#field. Такий вираз, це SyntaxError ще на етапі парсингу, навіть якщо об'єкт фізично містить це поле, бо його створив конструктор батька.

Теорія

TL;DR

  • #field належить одному конкретному тілу класу, а не прототипу, тому підклас його не бачить.
  • Звернення до this.#parentField усередині нащадка, це SyntaxError, а не undefined.
  • Однакові імена #secret у батька і в нащадка, це два різні незалежні поля.
  • Публічні методи батька, що читають своє приватне поле, успадковуються і чудово працюють на екземплярі нащадка.
  • Щоб поділитися даними з нащадками, використовують getter або метод, домовленість _field чи WeakMap.

Швидкий приклад

javascript
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 у звичайному сенсі: воно резолвиться лексично, як змінна, а не через властивість об'єкта.

Наслідок: однакові імена в різних класах, це різні поля без жодного зв'язку між собою.

javascript
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. Це не приватність, а угода «не чіпай ззовні», зате поле звичайне і повністю доступне нащадкам.

javascript
class Parent { _secret = 'available to subclasses'; showSecret() { console.log(this._secret); } } class Child extends Parent { reveal() { console.log(this._secret); // працює } }

Варіант 2: getter або метод батька. Приватність зберігається, а нащадок працює через інтерфейс і не знає деталей реалізації.

javascript
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. Дані лежать поза об'єктом, у замиканні модуля, тому ззовні недоступні, але батько може відкрити їх нащадкам через захищений аксесор.

javascript
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 віддає дані будь-кому, хто має посилання на саму мапу.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.