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

«Как с помощью замыканий реализовать инкапсуляцию в JavaScript?» — вопрос из категории JavaScript, который задают на 26% собеседований Node.js Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

В JavaScript я использую замыкания для создания приватного состояния и методов, что является основой модульного паттерна. Вот как я это реализую на практике:

Базовый пример с приватными переменными:

function createBankAccount(initialBalance) {
  // Приватные переменные, доступные только через замыкание
  let balance = initialBalance;
  let transactionHistory = [];

  // Публичные методы, возвращаемые наружу
  return {
    deposit: function(amount) {
      if (amount > 0) {
        balance += amount;
        transactionHistory.push({
          type: 'deposit',
          amount,
          timestamp: new Date()
        });
        return balance;
      }
      throw new Error('Deposit amount must be positive');
    },

    withdraw: function(amount) {
      if (amount > 0 && amount <= balance) {
        balance -= amount;
        transactionHistory.push({
          type: 'withdraw',
          amount,
          timestamp: new Date()
        });
        return balance;
      }
      throw new Error('Invalid withdrawal amount');
    },

    getBalance: function() {
      return balance;
    },

    getRecentTransactions: function(count = 5) {
      return transactionHistory.slice(-count);
    }
  };
}

// Использование
const myAccount = createBankAccount(1000);
console.log(myAccount.getBalance()); // 1000
myAccount.deposit(500);
console.log(myAccount.getBalance()); // 1500

// Попытка доступа к приватным полям напрямую
console.log(myAccount.balance); // undefined
console.log(myAccount.transactionHistory); // undefined

Более сложный пример с фабрикой объектов:

function createUserManager() {
  // Приватный кэш пользователей
  const usersCache = new Map();

  // Приватный метод валидации
  function validateUserData(userData) {
    const requiredFields = ['id', 'name', 'email'];
    return requiredFields.every(field => userData[field]);
  }

  // Публичный API
  return {
    addUser: function(userData) {
      if (!validateUserData(userData)) {
        throw new Error('Invalid user data');
      }

      usersCache.set(userData.id, {
        ...userData,
        createdAt: new Date(),
        updatedAt: new Date()
      });

      return userData.id;
    },

    getUser: function(userId) {
      const user = usersCache.get(userId);
      if (!user) {
        throw new Error(`User ${userId} not found`);
      }
      return { ...user }; // Возвращаем копию для защиты от изменений
    },

    updateUser: function(userId, updates) {
      const user = usersCache.get(userId);
      if (!user) {
        throw new Error(`User ${userId} not found`);
      }

      usersCache.set(userId, {
        ...user,
        ...updates,
        updatedAt: new Date()
      });
    },

    getStats: function() {
      return {
        totalUsers: usersCache.size,
        cacheSize: Buffer.byteLength(JSON.stringify([...usersCache.values()]))
      };
    }
  };
}

// Использование
const userManager = createUserManager();
const userId = userManager.addUser({
  id: 'user-123',
  name: 'John Doe',
  email: 'john@example.com'
});

console.log(userManager.getUser(userId));
console.log(userManager.getStats());

Преимущества такого подхода:

  1. Полная инкапсуляция: Приватные переменные недоступны извне
  2. Контроль доступа: Можно реализовать сложную логику валидации
  3. Состояние сохраняется: Объекты "помнят" своё состояние между вызовами
  4. Гибкость: Можно создавать множество независимых экземпляров

В современном Node.js я также использую классы с приватными полями (синтаксис #), но замыкания остаются полезными для более сложных сценариев инкапсуляции.