Пошук уроків, статей та іншого контенту
Приховуватимете внутрішній стан об’єкта за допомогою приватних полів і методів із префіксом #.
У JavaScript властивості об’єкта зазвичай доступні ззовні:
class User {
constructor(name, password) {
this.name = name;
this.password = password;
}
}
const user = new User("Olena", "secret");
console.log(user.password); // "secret"
user.password = "new-password";Такий стан об’єкта можна випадково або навмисно змінити з будь-якого місця програми. Якщо певні дані мають бути доступні лише самій реалізації класу, їх можна оголосити приватними.
Приватні поля та методи позначаються символом #:
class User {
#password;
constructor(password) {
this.#password = password;
}
}Приватні елементи:
доступні лише всередині класу, де їх оголошено;
не доступні через крапку або квадратні дужки;
не з’являються під час перебору властивостей;
не можуть бути створені динамічно після оголошення класу;
захищають внутрішню реалізацію від прямого втручання.
Приватне поле потрібно оголосити в тілі класу:
class BankAccount {
#balance = 0;
deposit(amount) {
this.#balance += amount;
}
getBalance() {
return this.#balance;
}
}
const account = new BankAccount();
account.deposit(100);
console.log(account.getBalance()); // 100У цьому прикладі #balance можна читати та змінювати лише всередині класу BankAccount.
Спроба звернутися до нього ззовні призведе до синтаксичної помилки:
console.log(account.#balance);Такий код навіть не запуститься. Приватні поля не є звичайними властивостями об’єкта.
Приватне поле можна оголосити без початкового значення та ініціалізувати в конструкторі:
class User {
#name;
constructor(name) {
this.#name = name;
}
getName() {
return this.#name;
}
}
const user = new User("Олена");
console.log(user.getName()); // "Олена"Якщо поле оголошено, але значення не призначено, воно має значення undefined:
class Example {
#value;
getValue() {
return this.#value;
}
}
console.log(new Example().getValue()); // undefinedЗапис із # не можна замінити квадратними дужками:
class User {
#name = "Олена";
getName() {
return this.#name;
}
}
const user = new User();
// user["#name"] — це зовсім інша властивість,
// а не доступ до приватного поля #name.
console.log(user["#name"]); // undefinedІм’я приватного поля визначається під час оголошення класу. Не можна зберегти його назву в змінній і звернутися до нього динамічно.
Приватним може бути не лише поле, а й метод. Для цього перед іменем методу також ставлять #:
class PasswordValidator {
#hasMinimumLength(password) {
return password.length >= 8;
}
isValid(password) {
return this.#hasMinimumLength(password);
}
}
const validator = new PasswordValidator();
console.log(validator.isValid("secret")); // false
console.log(validator.isValid("secret123")); // trueМетод #hasMinimumLength є внутрішньою деталлю класу. Користувачу класу достатньо знати про публічний метод isValid.
Приватні методи зручно використовувати для:
перевірки даних;
обчислення проміжних значень;
розділення складної логіки на невеликі частини;
приховування технічних деталей реалізації.
class ShoppingCart {
#items = [];
addItem(name, price, quantity = 1) {
if (!this.#isValidItem(name, price, quantity)) {
throw new Error("Некоректні дані товару");
}
this.#items.push({ name, price, quantity });
}
getTotal() {
return this.#items.reduce(
(total, item) => total + this.#calculateItemTotal(item),
0
);
}
#isValidItem(name, price, quantity) {
return (
typeof name === "string" &&
name.trim().length > 0 &&
Number.isFinite(price) &&
price >= 0 &&
Number.isInteger(quantity) &&
quantity > 0
);
}
#calculateItemTotal(item) {
return item.price * item.quantity;
}
}
const cart = new ShoppingCart();
cart.addItem("Книга", 300, 2);
cart.addItem("Зошит", 50, 3);
console.log(cart.getTotal()); // 750Зовнішній код працює лише з addItem і getTotal. Перевірка товару та обчислення його вартості приховані в приватних методах.
Приватні поля часто поєднують із публічними гетерами та методами для контрольованої зміни стану:
class Temperature {
#celsius = 0;
get celsius() {
return this.#celsius;
}
set celsius(value) {
if (!Number.isFinite(value)) {
throw new TypeError("Температура має бути числом");
}
this.#celsius = value;
}
get fahrenheit() {
return this.#celsius * 9 / 5 + 32;
}
}
const temperature = new Temperature();
temperature.celsius = 25;
console.log(temperature.celsius); // 25
console.log(temperature.fahrenheit); // 77Поле #celsius приховане, але клас надає безпечний публічний інтерфейс:
гетер celsius повертає значення;
сетер celsius перевіряє нове значення;
гетер fahrenheit обчислює похідне значення.
Це краще, ніж робити поле відкритим і покладатися на те, що інший код не запише некоректне значення.
Приватні елементи можуть належати не екземпляру, а самому класу. Для цього використовується ключове слово static:
class IdGenerator {
static #lastId = 0;
static next() {
IdGenerator.#lastId += 1;
return IdGenerator.#lastId;
}
}
console.log(IdGenerator.next()); // 1
console.log(IdGenerator.next()); // 2
console.log(IdGenerator.next()); // 3#lastId належить класу IdGenerator, а не окремим об’єктам.
Статичний приватний метод також викликається через клас:
class Logger {
static #format(message) {
return `[LOG] ${message}`;
}
static info(message) {
console.log(Logger.#format(message));
}
}
Logger.info("Застосунок запущено"); // [LOG] Застосунок запущеноВикликати #format ззовні неможливо.
Приватні поля належать конкретному класу, який їх оголосив. Навіть дочірній клас не має прямого доступу до приватного поля батьківського класу:
class Person {
#name;
constructor(name) {
this.#name = name;
}
getName() {
return this.#name;
}
}
class Employee extends Person {
printName() {
// this.#name — помилка:
// поле #name оголошене в Person, а не в Employee.
console.log(this.getName());
}
}
const employee = new Employee("Олена");
employee.printName(); // ОленаДочірній клас може користуватися публічними або захищеними через API методами батьківського класу, але не може напряму звернутися до його приватних елементів.
У дочірньому класі можна оголосити власне приватне поле з таким самим іменем:
class Parent {
#value = "значення Parent";
getParentValue() {
return this.#value;
}
}
class Child extends Parent {
#value = "значення Child";
getChildValue() {
return this.#value;
}
}
const child = new Child();
console.log(child.getParentValue()); // значення Parent
console.log(child.getChildValue()); // значення ChildЦе два різні приватні поля. Однакова назва не створює конфлікту, оскільки кожне поле належить своєму класу.
Для перевірки, чи належить об’єкт потрібному екземпляру класу, використовується оператор in із приватним іменем:
class User {
#name;
constructor(name) {
this.#name = name;
}
static isUser(value) {
return #name in value;
}
}
const user = new User("Олена");
console.log(User.isUser(user)); // true
console.log(User.isUser({})); // falseЦей синтаксис можна використовувати лише всередині класу, де оголошено приватне поле.
На відміну від перевірки через instanceof, така перевірка визначає, чи має об’єкт внутрішній приватний стан цього класу. Вона може бути корисною для захисту статичних методів або роботи з об’єктами, що походять від різних реалізацій.
Приватні поля не проходять через звичайний механізм властивостей JavaScript. Через це вони поводяться особливо під час використання Proxy.
class Counter {
#value = 0;
increment() {
this.#value += 1;
return this.#value;
}
}
const counter = new Counter();
const proxy = new Proxy(counter, {});
console.log(counter.increment()); // 1
// Помилка: під час виклику через proxy значення this
// стає proxy, а proxy не має приватного поля #value.
console.log(proxy.increment());Приватне поле належить оригінальному об’єкту counter, але не об’єкту-обгортці proxy. Це важливо враховувати під час використання проксі для логування, реактивності або контролю доступу.
Під час проєктування класу варто розділяти:
публічний інтерфейс — методи та властивості, якими користуються зовнішні частини програми;
приватну реалізацію — дані й допоміжні операції, які можуть змінитися без впливу на користувачів класу.
Наприклад:
class UserRepository {
#users = new Map();
add(user) {
this.#validateUser(user);
this.#users.set(user.id, user);
}
findById(id) {
return this.#users.get(id);
}
count() {
return this.#users.size;
}
#validateUser(user) {
if (!user || !Number.isInteger(user.id)) {
throw new TypeError("Користувач має містити числовий id");
}
if (typeof user.name !== "string" || user.name.trim() === "") {
throw new TypeError("Користувач має містити непорожнє ім’я");
}
}
}
const repository = new UserRepository();
repository.add({ id: 1, name: "Олена" });
console.log(repository.findById(1)); // { id: 1, name: "Олена" }
console.log(repository.count()); // 1Код, який використовує UserRepository, не знає, що всередині використовується Map. У майбутньому реалізацію можна замінити на масив, кеш або інше сховище, не змінюючи публічні методи.
До появи приватних полів розробники часто використовували підкреслення:
class User {
constructor(name) {
this._name = name;
}
}_name — це лише домовленість. JavaScript не забороняє звертатися до такого поля:
const user = new User("Олена");
console.log(user._name); // доступ дозволений
user._name = "Інше ім’я";Натомість #name є справді приватним:
class User {
#name = "Олена";
getName() {
return this.#name;
}
}
const user = new User();
// user.#name — синтаксична помилкаПідкреслення все ще може бути доречним, якщо поле має бути доступним дочірнім класам або якщо приватність контролюється домовленістю команди. Але коли потрібне обмеження на рівні мови, слід використовувати #.
class Example {
constructor(value) {
this.#value = value;
}
}Цей код некоректний, оскільки #value не оголошено в класі.
Правильний варіант:
class Example {
#value;
constructor(value) {
this.#value = value;
}
}class User {
#name = "Олена";
}
const user = new User();
console.log(user["#name"]); // undefined"#name" — це звичайний рядок, а не спеціальний синтаксис доступу до приватного поля.
class Validator {
#validate() {
return true;
}
}
const validator = new Validator();
// validator.#validate() — синтаксична помилкаЯкщо метод має викликатися ззовні, створіть публічний метод, який звертається до приватного:
class Validator {
#validate() {
return true;
}
isValid() {
return this.#validate();
}
}class A {
#value = 10;
}
class B {
read(object) {
// object.#value — помилка.
// Поле #value оголошене в A, а не в B.
}
}Приватне ім’я має область доступу в межах класу, де його оголошено.
thisПриватне поле можна прочитати лише через правильний екземпляр класу:
class Counter {
#value = 0;
increment() {
this.#value += 1;
return this.#value;
}
}
const counter = new Counter();
const increment = counter.increment;
// increment(); — помилка, оскільки this не посилається на counter.Якщо метод передається як callback, за потреби прив’яжіть контекст:
const increment = counter.increment.bind(counter);
console.log(increment()); // 1Приватні поля та методи доречні, коли:
об’єкт має внутрішній стан, який не можна змінювати напряму;
потрібно гарантувати перевірку даних перед зміною;
реалізація класу містить складні допоміжні операції;
важливо приховати структуру внутрішнього сховища;
API класу має бути невеликим і зрозумілим;
зміни внутрішньої реалізації не повинні ламати код користувачів класу.
Не кожне поле потрібно робити приватним. Якщо властивість є частиною публічного контракту об’єкта, її краще залишити публічною або надати для неї контрольований гетер чи сетер.
Приватні поля оголошуються з префіксом #, наприклад #balance.
Приватні поля потрібно оголосити всередині класу до використання.
Доступ до приватного поля можливий лише в класі, де його оголошено.
Приватні методи записуються як #methodName().
Публічні методи можуть використовувати приватні поля та методи, формуючи контрольований API.
#name — справді приватний елемент, а _name — лише домовленість.
Приватні елементи батьківського класу недоступні безпосередньо дочірньому класу.
Статичні приватні поля та методи належать самому класу й позначаються як static #name.
Приватні елементи допомагають приховати реалізацію, захистити стан об’єкта та зменшити зв’язність коду.