Инкапсуляция
Инкапсуляция — принцип ООП, ограничивающий прямой доступ к внутреннему состоянию объекта. Данные и методы, работающие с ними, объединяются, а внешний код взаимодействует через публичный интерфейс.
Зачем нужно
Инкапсуляция защищает данные от некорректного изменения, скрывает детали реализации, позволяет изменять внутреннюю логику без влияния на внешний код. Это основа модульности и надёжности программ.
Где используется
Классы и модули, библиотеки API, компоненты UI, модели данных, сервисы, state management.
Предпосылки
Классы, Замыкания (Closures), this
Приватные поля (#)
ES2022 ввёл настоящие приватные поля:
class User {
#password;
#loginAttempts = 0;
constructor(name, password) {
this.name = name; // публичное
this.#password = password; // приватное
}
#hashPassword(raw) {
// Приватный метод
return `hashed_${raw}`;
}
authenticate(input) {
this.#loginAttempts++;
if (this.#loginAttempts > 5) {
throw new Error('Слишком много попыток');
}
return this.#hashPassword(input) === this.#hashPassword(this.#password);
}
get attempts {
return this.#loginAttempts;
}
}
const user = new User('Иван', 'secret123');
console.log(user.authenticate('secret123')); // true
console.log(user.attempts); // 1
// user.#password; // SyntaxError
// user.#hashPassword; // SyntaxError
Проверка наличия приватного поля
class Brand {
#name;
constructor(name) { this.#name = name; }
static isBrand(obj) {
return #name in obj; // true только для экземпляров Brand
}
}
console.log(Brand.isBrand(new Brand('Test'))); // true
console.log(Brand.isBrand({ name: 'Fake' })); // false
Замыкания для приватности
До приватных полей (#) использовали замыкания:
function createWallet(initialBalance) {
let balance = initialBalance; // приватная переменная
const history = ; // приватный массив
return {
deposit(amount) {
if (amount <= 0) throw new Error('Положительная сумма');
balance += amount;
history.push({ type: 'deposit', amount, date: new Date });
},
withdraw(amount) {
if (amount > balance) throw new Error('Недостаточно средств');
balance -= amount;
history.push({ type: 'withdraw', amount, date: new Date });
},
getBalance { return balance; },
getHistory { return [...history]; } // копия
};
}
const wallet = createWallet(1000);
wallet.deposit(500);
wallet.withdraw(200);
console.log(wallet.getBalance); // 1300
// balance и history недоступны
WeakMap для приватных данных
const _private = new WeakMap();
class Settings {
constructor(config) {
_private.set(this, {
config: { ...config },
listeners:
});
}
get(key) {
return _private.get(this).config[key];
}
set(key, value) {
const data = _private.get(this);
const oldValue = data.config[key];
data.config[key] = value;
data.listeners.forEach(fn => fn(key, value, oldValue));
}
onChange(listener) {
_private.get(this).listeners.push(listener);
}
}
const settings = new Settings({ theme: 'dark', lang: 'ru' });
settings.onChange((key, val) => console.log(`${key} = ${val}`));
settings.set('theme', 'light'); // "theme = light"
// Нет доступа к внутреннему config и listeners
Getters/Setters для валидации
class Person {
#name;
#age;
constructor(name, age) {
this.name = name; // через setter
this.age = age; // через setter
}
get name { return this.#name; }
set name(value) {
if (typeof value !== 'string' || value.trim().length === 0) {
throw new Error('Имя должно быть непустой строкой');
}
this.#name = value.trim();
}
get age { return this.#age; }
set age(value) {
if (!Number.isInteger(value) || value < 0 || value > 150) {
throw new Error('Возраст: целое число 0-150');
}
this.#age = value;
}
toString {
return `${this.#name}, ${this.#age} лет`;
}
}
const person = new Person(' Иван ', 25);
console.log(person.name); // "Иван" (trimmed)
// person.age = -5; // Error: Возраст: целое число 0-150
Readonly свойства
class Config {
#data;
constructor(data) {
this.#data = Object.freeze({ ...data });
}
get(key) {
return this.#data[key];
}
// Нет setter — данные неизменяемы
getAll {
return { ...this.#data }; // копия
}
}
const config = new Config({ apiUrl: 'https://api.example.com', timeout: 5000 });
console.log(config.get('apiUrl')); // 'https://api.example.com'
// Невозможно изменить данные
Соглашение об именовании (_)
Неформальная конвенция — не настоящая приватность:
class Service {
constructor {
this._cache = new Map(); // "приватное" по конвенции
this._isInitialized = false;
}
_validate(data) { // "приватный" метод
return data != null;
}
process(data) {
if (!this._validate(data)) throw new Error('Invalid');
// ...
}
}
// Технически _cache доступен, но конвенция говорит "не трогай"
Сравнение подходов
| Подход | Настоящая приватность | Производительность | Наследование |
|---|---|---|---|
#private |
Да | Отличная | Не наследуется |
| Замыкание | Да | Хорошая | Не применимо |
| WeakMap | Да | Хорошая | Работает |
_convention |
Нет | Отличная | Наследуется |
| Symbol | Нет (Reflect) | Хорошая | Наследуется |
Частые ошибки
1. Возврат ссылки на приватный объект
class Bad {
#items = [1, 2, 3];
getItems { return this.#items; } // возвращает ссылку!
}
const bad = new Bad();
bad.getItems.push(4); // мутирует приватные данные!
// Правильно — возвращать копию
class Good {
#items = [1, 2, 3];
getItems { return [...this.#items]; }
}
2. Приватные поля не наследуются
class Parent {
#secret = 42;
getSecret { return this.#secret; }
}
class Child extends Parent {
showSecret {
// return this.#secret; // SyntaxError!
return this.getSecret; // Через публичный метод — OK
}
}
3. Публичный API слишком широкий
// Плохо — всё публично
class UserService {
cache = {};
db = null;
fetchFromDB(id) { /* ... */ }
validateCache(id) { /* ... */ }
getUser(id) { /* ... */ }
}
// Хорошо — только необходимый публичный API
class UserService {
#cache = {};
#db = null;
#fetchFromDB(id) { /* ... */ }
#validateCache(id) { /* ... */ }
getUser(id) { /* единственный публичный метод */ }
}
Практика
- Создай класс
Stackс приватным массивом и методами push, pop, peek, size - Реализуй класс с readonly-свойствами через getters без setters
- Перепиши замыкание-модуль на класс с приватными полями
- Создай класс
Validatorс приватными правилами и публичнымvalidate(data) - Сравни размер: класс с #полями vs WeakMap-подход
Инкапсуляция vs сокрытие
автор принципиально разделяет:
- Инкапсуляция — структурный принцип: объединение полей и поведения в одну абстракцию (контейнер с состоянием + методами).
- Сокрытие — механизм защиты: невозможность извне дотянуться до внутренностей.
«Сокрытие — это в итоге всегда
if. Если вы где-то пишетеprotectedилиprivate, это всё равно где-то происходитif. Просто от вас немножко скрыто» (8OuzIAuMfjw).
«От приличных людей не нужно никакого сокрытия делать. Подчёркивание — соглашение, не защита» (r4ReQlVtfgQ).
Это две разные идеи. Можно инкапсулировать без сокрытия (через _underscore) и сокрывать без полной инкапсуляции.
Состояние vs поведение
Инкапсуляция объединяет:
- Состояние — поля объекта (
x,y,name,cache) - Поведение — методы (
move,draw,serialize) - Опционально — события (
EventEmitter, observable)
struct/record без методов — частичная инкапсуляция (только данные). Полная — класс с методами.
Race condition в инкапсулированном объекте
автор (b0k8gvQ0J0Q) показывает: инкапсуляция не защищает от race condition при async-доступе. Нужны блокировки:
class Point {
#x; #y; #lock = new Lock();
constructor(x, y) { this.#x = x; this.#y = y; }
async move(dx, dy) {
await this.#lock.enter;
this.#x += dx; this.#y += dy;
this.#lock.leave;
}
}
Альтернатива в браузере — Web Locks API (navigator.locks.request('key', async () => ...)).
Связанные темы
- Классы
- Замыкания (Closures)
- Полиморфизм
- Module Pattern
- Прототипы
- Абстракция
- Номинальная vs структурная типизация
- Symbol и приватность
Источники
- Timur · ООП построение абстракций, инкапсуляция и сокрытие (2020-03-03)
- Timur · Object-oriented programming (2020-02-26)
- Timur · Comparison of OOP and FP styles in JavaScript (2025-11-24)
- Timur · Номинальная и структурная типизация, инкапсуляция, сокрытие (2025-11-01)
- MDN — Private class features
- JavaScript.info — Приватные и защищённые свойства