Приватні поля поза класом
Звернутися до приватної властивості класу ззовні неможливо: obj.#field поза тілом класу це недопустимий синтаксис, і рушій кидає SyntaxError ще до старту виконання. Це не домовленість на кшталт префікса _, а справжня мовна приватність (ES2022), яку не обійти ні через рядковий ключ, ні через Reflect, ні через серіалізацію.
Теорія
TL;DR
obj.#fieldпоза класом, цеSyntaxErrorна етапі парсингу, а не помилка під час виконання.obj['#field']не є зверненням до приватного поля: воно повернеundefinedабо створить звичайну публічну властивість з ім'ям'#field'.- Приватні поля не потрапляють у
Object.keys,Object.entries,Object.getOwnPropertyNames,Reflect.ownKeysіfor...in. JSON.stringifyприватні поля ігнорує: об'єкт лише з приватними полями серіалізується як{}.- Єдиний легальний доступ, це публічні методи, getter та setter, оголошені в тому самому класі.
Швидкий приклад
class User {
#password = 'secret-1';
checkPassword(pwd) {
return pwd === this.#password;
}
}
const u = new User();
console.log(u.checkPassword('secret-1')); // true, доступ через метод класу
// console.log(u.#password); // SyntaxError: Private field '#password'
// // must be declared in an enclosing classРядок з u.#password навіть не виконається: достатньо, щоб він просто був у файлі, і весь модуль не пройде парсинг.
Чому це помилка парсингу, а не рантайму
Приватність у JavaScript реалізована на рівні синтаксису, а не через домовленість про іменування. Коли рушій бачить токен #field, він робить дві перевірки ще на етапі компіляції:
- Чи оголошене це приватне ім'я в лексичному оточенні класу, всередині якого стоїть вираз.
- Якщо ні, він кидає
SyntaxErrorі навіть не намагається виконати код.
Саме тому таку помилку не можна перехопити через try/catch навколо звернення: до моменту, коли try міг би спрацювати, скрипт уже не скомпілювався.
class Example {
#value = 42;
}
const e = new Example();
try {
// console.log(e.#value); // розкоментування ламає ВЕСЬ файл, а не цей блок
} catch (err) {
console.log('сюди ми не потрапимо');
}Обхідні шляхи не працюють
Приватні поля не є звичайними властивостями об'єкта, тому жоден спосіб перелічування їх не покаже.
class User {
#password = 'secret-1';
}
const u = new User();
console.log(u['#password']); // undefined
console.log(Object.keys(u)); // []
console.log(Object.entries(u)); // []
console.log(Object.getOwnPropertyNames(u)); // []
console.log(Reflect.ownKeys(u)); // []
for (const key in u) {
console.log(key); // жодної ітерації
}Не допоможе й серіалізація: приватні поля не потрапляють у JSON.
class Account {
#balance = 1000;
}
const acc = new Account();
console.log(JSON.stringify(acc)); // '{}'Це зручна властивість: приватні поля не «протікають» у логи та в тіло HTTP-відповіді випадково, бо їх немає в результаті
JSON.stringify.
Приватне поле не можна підмінити
Запис у приватне поле ззовні так само є синтаксичною помилкою. А запис через рядковий ключ створює зовсім іншу, публічну властивість, яка з приватним полем ніяк не пов'язана.
class Example {
#value = 42;
getValue() {
return this.#value;
}
}
const e = new Example();
// e.#value = 100; // SyntaxError
e['#value'] = 100; // створює нову звичайну властивість
console.log(e); // Example { '#value': 100 }
console.log(e.getValue()); // 42, справжнє приватне поле не змінилосяТобто зовні можна лише «забруднити» об'єкт схожим на вигляд ключем, але реальний стан класу лишається недоторканим.
Єдиний правильний доступ: публічний інтерфейс
Якщо приватне значення потрібне назовні, клас сам вирішує, у якому вигляді його віддати. Для цього є методи, getter та setter, які мають доступ до #field, бо оголошені в тому самому класі.
class User {
#password = 'secret-1';
get maskedPassword() {
return '*'.repeat(this.#password.length);
}
checkPassword(pwd) {
return pwd === this.#password;
}
setPassword(newPwd) {
if (newPwd.length < 5) {
throw new Error('Пароль занадто короткий');
}
this.#password = newPwd;
}
}
const u = new User();
console.log(u.maskedPassword); // '********'
console.log(u.checkPassword('secret-1')); // true
u.setPassword('secret-2');Поле лишається захищеним, але клас надає контрольований, безпечний доступ: маскування при читанні та валідацію при записі.
Перевірка наявності приватного поля
Єдина операція над #field, дозволена не через this, це перевірка #field in obj, але й вона працює тільки всередині тіла класу. Її використовують як «brand check», щоб зрозуміти, чи є довільний об'єкт екземпляром саме цього класу.
class User {
#password = 'secret-1';
static isUser(obj) {
return #password in obj; // true лише для екземплярів User
}
}
console.log(User.isUser(new User())); // true
console.log(User.isUser({})); // falseПоза класом такий вираз знову буде SyntaxError, тож це не спосіб дістати значення ззовні, а спосіб безпечно перевірити тип усередині класу.
Підсумкова таблиця
| Дія ззовні класу | Результат |
|---|---|
obj.#field | SyntaxError на етапі парсингу |
obj.#field = 1 | SyntaxError на етапі парсингу |
obj['#field'] | undefined, або створює окрему публічну властивість |
Object.keys(obj) | приватного поля не видно |
Object.getOwnPropertyNames(obj) | приватного поля не видно |
Reflect.ownKeys(obj) | приватного поля не видно |
for...in | приватного поля не видно |
JSON.stringify(obj) | приватного поля не видно |
| метод, getter або setter класу | працює, це єдиний шлях |
Типові помилки
- Плутати
#fieldз угодою_field. Властивість_password, це звичайне публічне поле, його видно вObject.keysі його можна перезаписати.#passwordнедоступне фізично. - Думати, що
obj['#field']це те саме поле. Це окремий рядковий ключ; запис через нього створює новий публічний слот і не змінює приватного стану. - Пробувати перехопити помилку через
try/catch.SyntaxErrorвиникає до виконання, тому впаде весь файл або модуль, а не окремий блок. - Розраховувати, що приватні поля потраплять у JSON. Після
JSON.stringifyвони зникають, тому DTO треба формувати явно, наприклад методомtoJSON(). - Оголошувати приватне поле лише в конструкторі. Приватне ім'я має бути оголошене в тілі класу (
#password = ...або#password;), інакше присвоєнняthis.#passwordтеж стане синтаксичною помилкою. - Очікувати успадкування приватних полів. Підклас не бачить
#fieldбатьківського класу: приватні імена належать конкретному тілу класу, а не ланцюжку прототипів.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.