Как с помощью замыканий реализовать инкапсуляцию в JavaScript?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Инкапсуляция в JavaScript через замыкания (Closures)
Замыкания (Closures) — это мощный механизм JavaScript для создания приватных переменных и функций. Они позволяют реализовать инкапсуляцию ещё до того, как в языке появились приватные поля (# синтаксис в TypeScript/ES2022).
Основная идея замыканий
Замыкание — это функция, которая имеет доступ к переменным из своего внешнего (родительского) контекста, даже после того, как этот контекст завершил выполнение.
// Базовый пример замыкания
function createCounter() {
let count = 0; // Приватная переменная
return {
increment: () => ++count,
decrement: () => --count,
getCount: () => count,
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.decrement()); // 1
console.log(counter.getCount()); // 1
// count не доступна напрямую!
console.log(counter.count); // undefined
Ключевые моменты:
count— приватная переменная, недоступна снаружиincrement,decrement,getCount— публичные методы- Замыкание сохраняет доступ к
countво всех методах
Реальный пример: BankAccount
// ❌ Плохо — без инкапсуляции
class BankAccountBad {
balance = 0;
}
const badAccount = new BankAccountBad();
badAccount.balance = -10000; // Можно установить невалидное значение!
// ✅ Хорошо — с инкапсуляцией через замыкания
function createBankAccount(initialBalance: number) {
// Приватные переменные
let balance = initialBalance;
const transactions: Array<{ type: 'deposit' | 'withdraw', amount: number, date: Date }> = [];
// Приватная функция
function validateAmount(amount: number): void {
if (amount <= 0) {
throw new Error('Amount must be positive');
}
if (!Number.isFinite(amount)) {
throw new Error('Invalid amount');
}
}
// Публичные методы (возвращаем объект)
return {
deposit(amount: number): number {
validateAmount(amount);
balance += amount;
transactions.push({ type: 'deposit', amount, date: new Date() });
return balance;
},
withdraw(amount: number): number {
validateAmount(amount);
if (amount > balance) {
throw new Error('Insufficient funds');
}
balance -= amount;
transactions.push({ type: 'withdraw', amount, date: new Date() });
return balance;
},
getBalance(): number {
return balance;
},
getTransactions(): typeof transactions {
return [...transactions]; // Возвращаем копию, не оригинал!
},
};
}
// Использование
const account = createBankAccount(1000);
console.log(account.deposit(500)); // 1500
console.log(account.withdraw(200)); // 1300
console.log(account.getBalance()); // 1300
console.log(account.getTransactions()); // История всех транзакций
// Попытка нарушить инкапсуляцию
account.balance = -5000; // ❌ account.balance не существует!
console.log(account.balance); // undefined
Паттерн Module Pattern
Это расширение идеи замыканий для создания модулей с приватным и публичным интерфейсом.
// Module Pattern в JavaScript
const UserModule = (() => {
// Приватные переменные (доступны только внутри модуля)
const users: Map<string, { id: string; name: string; password: string }> = new Map();
let nextId = 1;
// Приватные функции
function hashPassword(password: string): string {
// Простой пример (в реальности используй bcrypt)
return Buffer.from(password).toString('base64');
}
function findUserById(id: string) {
return users.get(id);
}
// Публичный API (возвращаем объект с публичными методами)
return {
createUser(name: string, password: string) {
const id = String(nextId++);
const hashedPassword = hashPassword(password);
users.set(id, { id, name, password: hashedPassword });
return { id, name }; // Не возвращаем пароль!
},
authenticateUser(name: string, password: string): boolean {
let found = false;
for (const user of users.values()) {
if (user.name === name && user.password === hashPassword(password)) {
found = true;
break;
}
}
return found;
},
getUserCount(): number {
return users.size;
},
// Вспомогательный метод (для примера)
getAllUserNames(): string[] {
return Array.from(users.values()).map(u => u.name);
},
};
})(); // IIFE — Immediately Invoked Function Expression
// Использование
UserModule.createUser('alice', 'password123');
UserModule.createUser('bob', 'secret456');
console.log(UserModule.authenticateUser('alice', 'password123')); // true
console.log(UserModule.authenticateUser('alice', 'wrong')); // false
console.log(UserModule.getUserCount()); // 2
console.log(UserModule.getAllUserNames()); // ['alice', 'bob']
// Попытка доступа к приватным данным
console.log(UserModule.users); // undefined
console.log(UserModule.nextId); // undefined
Factory Pattern с замыканиями
// Factory для создания объектов с инкапсуляцией
function createLogger(prefix: string) {
// Приватные переменные
const logs: string[] = [];
let isEnabled = true;
// Приватная функция
function formatMessage(level: string, message: string): string {
return `[${new Date().toISOString()}] [${prefix}] [${level}] ${message}`;
}
// Публичный API
return {
log(message: string): void {
if (!isEnabled) return;
const formatted = formatMessage('INFO', message);
logs.push(formatted);
console.log(formatted);
},
error(message: string): void {
if (!isEnabled) return;
const formatted = formatMessage('ERROR', message);
logs.push(formatted);
console.error(formatted);
},
getLogs(): string[] {
return [...logs]; // Возвращаем копию
},
disable(): void {
isEnabled = false;
},
enable(): void {
isEnabled = true;
},
};
}
// Использование
const logger = createLogger('API');
logger.log('Server started');
logger.log('Request received');
logger.error('Database error');
console.log(logger.getLogs());
// output:
// [2025-03-28T10:30:00.000Z] [API] [INFO] Server started
// [2025-03-28T10:30:01.000Z] [API] [INFO] Request received
// [2025-03-28T10:30:02.000Z] [API] [ERROR] Database error
logger.disable();
logger.log('This will not be logged'); // Ничего не выведется
Сравнение: Замыкания vs Приватные поля (ES2022)
// Способ 1: Замыкания (работает везде)
function createCounter1() {
let count = 0;
return {
increment: () => ++count,
get: () => count,
};
}
// Способ 2: Приватные поля (TypeScript / ES2022)
class Counter {
#count = 0; // Приватное поле
increment(): number {
return ++this.#count;
}
get(): number {
return this.#count;
}
}
// Сравнение
const c1 = createCounter1();
const c2 = new Counter();
// Функциональность одинакова
console.log(c1.increment()); // 1
console.log(c2.increment()); // 1
// Оба защищают приватные данные
console.log(c1.count); // undefined (замыкание)
console.log(c2['#count']); // undefined (приватное поле)
// Разница в производительности (приватные поля быстрее)
// Но замыкания работают в более старых браузерах
Практический пример: Конфигурация с валидацией
function createConfig(initialConfig: Record<string, any>) {
// Приватные переменные
let config = { ...initialConfig };
const validators: Record<string, (value: any) => boolean> = {};
// Приватная функция
function validateKey(key: string, value: any): void {
const validator = validators[key];
if (validator && !validator(value)) {
throw new Error(`Invalid value for ${key}`);
}
}
// Публичный API
return {
set(key: string, value: any): void {
validateKey(key, value);
config[key] = value;
},
get(key: string): any {
return config[key];
},
setValidator(key: string, fn: (value: any) => boolean): void {
validators[key] = fn;
},
getAll(): Readonly<typeof config> {
return Object.freeze({ ...config });
},
};
}
// Использование
const config = createConfig({ port: 3000, debug: false });
config.setValidator('port', (value) => typeof value === 'number' && value > 0);
config.setValidator('debug', (value) => typeof value === 'boolean');
config.set('port', 5000); // OK
console.log(config.get('port')); // 5000
config.set('port', -1); // ❌ Error: Invalid value for port
config.set('debug', 'yes'); // ❌ Error: Invalid value for debug
Преимущества замыканий для инкапсуляции
✅ Истинно приватные переменные — невозможно получить доступ снаружи ✅ Простота — не нужны сложные синтаксис ✅ Гибкость — легко добавлять методы и логику ✅ Совместимость — работает в старых версиях JavaScript ✅ Performance — замыкания оптимизированы в современных JS движках
Недостатки и когда избегать
⚠️ Сложность в больших объёмах — много вложенных функций сложнее читать ⚠️ Отладка — сложнее смотреть приватные переменные в debugger ⚠️ Использование памяти — замыкания сохраняют ссылки на внешние переменные
Итоги
Замыкания — это мощный инструмент для инкапсуляции в JavaScript: ✓ Создают истинно приватные переменные ✓ Работают без специальных синтаксисов ✓ Позволяют реализовать Module Pattern ✓ Идеальны для Factory функций ✓ Совместимы со всеми версиями JavaScript