Замыкания: паттерны на практике

Замыкания на практике: счётчики, модуль, мемоизация и настоящая приватность данных.

Замыкание (closure) — функция вместе с захваченными переменными окружения, в котором она была создана. Эти переменные живут, пока жива функция.

Счётчик: переменная живёт между вызовами

Локальная переменная обычно умирает с завершением функции. Но если вернуть вложенную функцию, которая на неё ссылается, переменная сохранится — это и есть замыкание. Каждый вызов фабрики создаёт отдельное окружение:

function createCounter() {
  let count = 0;
  return {
    increment() { count++; return count; },
    decrement() { count--; return count; },
    value() { return count; }
  };
}

const c = createCounter();
console.log(c.increment());
console.log(c.increment());
console.log(c.decrement());
console.log("итог:", c.value());

Вывод:

1
2
1
итог: 1

Паттерн «Модуль»: приватное и публичное

Замыкание — основа модульного паттерна: внутри функции прячем приватное состояние и возвращаем только публичный API. Снаружи к count не дотянуться — нет ссылки.

Мемоизация: кэш в замыкании

Мемоизация ускоряет дорогие чистые функции, запоминая результат для каждого аргумента. Кэш живёт в замыкании и не виден снаружи:

function memoize(fn) {
  const cache = new Map();
  return function(n) {
    if (cache.has(n)) {
      console.log("из кэша:", n);
      return cache.get(n);
    }
    const result = fn(n);
    cache.set(n, result);
    return result;
  };
}

const square = memoize(n => n * n);
console.log(square(4));
console.log(square(4));
console.log(square(5));

Вывод:

16
из кэша: 4
16
25

Второй вызов square(4) не пересчитывал, а взял из кэша — отсюда строка «из кэша: 4».

Приватность через WeakMap

До приватных полей (#field) данные класса прятали в замыкании или в WeakMap, где ключ — сам экземпляр. Снаружи такие данные недоступны:

const _balance = new WeakMap();

class Account {
  constructor(initial) { _balance.set(this, initial); }
  deposit(sum) { _balance.set(this, _balance.get(this) + sum); }
  getBalance() { return _balance.get(this); }
}

const acc = new Account(100);
acc.deposit(50);
console.log("баланс:", acc.getBalance());
console.log("приватно? напрямую не достать:", acc.balance);

Вывод:

баланс: 150
приватно? напрямую не достать: undefined

Классическая ловушка с циклом

Знаменитый баг: var в цикле создаёт одну переменную на все итерации, поэтому все колбэки видят её финальное значение. Решение — let, который создаёт новую привязку на каждой итерации.

Итог

  • Замыкание сохраняет переменные окружения функции после её создания.
  • На замыканиях строятся счётчики, модули, мемоизация и приватность.
  • В циклах используйте let, а не var, чтобы каждый колбэк захватил своё значение.
Проверьте себя
1. Что такое замыкание?
AФункция без аргументов
BФункция вместе с захваченными переменными её окружения
CСпособ закрыть программу
DОбъект с приватными методами
2. Зачем в memoize кэш хранят в Map внутри замыкания?
AЧтобы он был глобальным
BЧтобы он был приватным и сохранялся между вызовами
CЧтобы ускорить сборку мусора
DЭто обязательное требование синтаксиса
3. Почему в цикле для колбэков лучше let, а не var?
Alet работает быстрее
Blet создаёт новую привязку на каждой итерации, var — одну общую
Cvar не поддерживается в браузерах
DРазницы нет
Поддержать проект