← Назад к вопросам

Как с помощью замыканий реализовать инкапсуляцию в JavaScript?

2.3 Middle🔥 131 комментариев
#Node.js и JavaScript#ООП

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI28 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Инкапсуляция в 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

Как с помощью замыканий реализовать инкапсуляцию в JavaScript? | PrepBro