Приватность через замыкания

Приватность через замыкания — техника инкапсуляции данных, при которой переменные объявляются в теле функции и недоступны снаружи, но доступны через возвращаемые публичные методы.

Зачем нужно

До появления приватных полей классов (#field, ES2022) замыкание было единственным надёжным способом создать по-настоящему приватные данные в JavaScript. Даже сейчас эта техника актуальна для функционального стиля и фабричных функций, где требуется инкапсуляция без классов.

Где используется

  • Фабричные функции (factory functions) вместо классов
  • Модульный паттерн (Module Pattern)
  • Счётчики, кеши, очереди с ограниченным доступом
  • Хранение токенов, сессий с контролем доступа

Базовый паттерн

function createCounter(initial = 0) {
  // count — приватная переменная, недоступна снаружи
  let count = initial;

  return {
    // Публичные методы — замыкают count
    increment { return ++count; },
    decrement { return --count; },
    getCount  { return count; },
    reset     { count = initial; }
  };
}

const counter = createCounter(10);
counter.increment; // 11
counter.increment; // 12
counter.decrement; // 11

// Прямой доступ недоступен
console.log(counter.count); // undefined
counter.count = 999;         // не изменит внутреннее count!
console.log(counter.getCount); // 11

Модульный паттерн (IIFE)

const UserStore = (function {
  // Приватное состояние
  const users = new Map();
  let nextId = 1;

  // Приватные вспомогательные функции
  function validate(user) {
    return user.name && user.email;
  }

  // Публичный API
  return {
    add(userData) {
      if (!validate(userData)) throw new Error('Невалидные данные');
      const id = nextId++;
      users.set(id, { ...userData, id });
      return id;
    },
    get(id) {
      return users.get(id) || null;
    },
    getAll {
      return [...users.values()]; // копия, не оригинал
    },
    count {
      return users.size;
    }
  };
});

UserStore.add({ name: 'Иван', email: 'ivan@mail.ru' });
UserStore.add({ name: 'Анна', email: 'anna@mail.ru' });

console.log(UserStore.count); // 2
console.log(UserStore.get(1));  // { name: 'Иван', email: '...', id: 1 }

// Приватные данные недоступны
console.log(UserStore.users); // undefined

Геттеры и сеттеры с валидацией

function createPerson(initialName, initialAge) {
  let name = initialName;
  let age = initialAge;

  return {
    getName { return name; },
    setName(newName) {
      if (typeof newName !== 'string' || !newName.trim()) {
        throw new Error('Имя должно быть непустой строкой');
      }
      name = newName.trim();
    },
    getAge { return age; },
    setAge(newAge) {
      if (!Number.isInteger(newAge) || newAge < 0 || newAge > 150) {
        throw new Error('Некорректный возраст');
      }
      age = newAge;
    },
    toString {
      return `${name}, ${age} лет`;
    }
  };
}

const person = createPerson('Иван', 25);
person.setName('Анна');
person.setAge(30);
console.log(person.toString()); // Анна, 30 лет

person.setAge(-5); // Error: Некорректный возраст

Сравнение с приватными полями классов (ES2022)

// Замыкание — приватность через функциональный подход
function createBankAccount(balance) {
  let _balance = balance;
  return {
    deposit(n) { _balance += n; },
    getBalance { return _balance; }
  };
}

// Приватные поля класса — синтаксическая поддержка в JS
class BankAccount {
  #balance; // по-настоящему приватное поле

  constructor(balance) { this.#balance = balance; }
  deposit(n) { this.#balance += n; }
  getBalance { return this.#balance; }
}

// Разница: в factory function каждый экземпляр создаёт новые методы
// В классе методы живут на прототипе — эффективнее при большом числе объектов

Частые ошибки

1. Возврат примитива вместо объекта теряет доступ к методам

function makeValue(initial) {
  let val = initial;
  return val; // возвращает копию, не замыкание!
}

const v = makeValue(5);
// v — просто число, нет доступа к val

2. Мутация возвращённых данных нарушает инкапсуляцию

function createList() {
  const items = ;
  return {
    getItems { return items; } // возвращаем ссылку!
  };
}

const list = createList;
const leaked = list.getItems;
leaked.push('внешний элемент'); // мутируем внутренний массив!

// Решение: возвращать копию
getItems { return [...items]; }

Связанные темы

Ресурсы