← назад к разделу

Замыкание (closure) — самая знаменитая «сложная» тема JavaScript, хотя определение помещается в одну фразу: функция сохраняет доступ к области видимости, в которой была создана, — даже когда выполняется совсем в другом месте. Если вы понимаете лексический scope, вы уже понимаете замыкания — осталось увидеть следствия.

Функция уносит область с собой

function makeCounter() {
  let count = 0;
  return function () {
    count++;
    return count;
  };
}

const counter = makeCounter(); // makeCounter отработала и «умерла»...
counter(); // 1
counter(); // 2 — ...но count жив!

По наивной логике count должен исчезнуть вместе с завершением makeCounter. Но возвращённая функция ссылается на него — и движок сохраняет переменную живой, пока жива функция. Это и есть замыкание: не копия значения, а живая ссылка на переменную.

Два независимых счётчика — два независимых замыкания:

const a = makeCounter();
const b = makeCounter();
a(); a(); // 2
b();      // 1 — у каждого вызова makeCounter своя count

Зачем: приватное состояние и фабрики

Приватность. До появления #полей в классах замыкание было единственным способом спрятать данные: к count снаружи не добраться никак — нет ни имени, ни ссылки. Только через функции, которым «разрешено».

function createWallet(initial) {
  let balance = initial;
  return {
    deposit: (n) => (balance += n),
    getBalance: () => balance,
  };
}
const w = createWallet(100);
w.balance;      // undefined — снаружи не существует
w.getBalance(); // 100

Фабрики функций. Функция, настроенная параметрами один раз:

const multiplyBy = (factor) => (n) => n * factor;
const double = multiplyBy(2);
const triple = multiplyBy(3);
double(5); // 10
triple(5); // 15

Каждая произведённая функция замкнула свой factor. Этот паттерн повсюду: обработчики с параметрами, debounce/throttle, каррирование, мидлвары.

Колбэки с контекстом. Любой обработчик события или таймер, использующий внешние переменные, — замыкание:

function setupSearch(input) {
  let lastQuery = "";
  input.addEventListener("input", () => {
    lastQuery = input.value; // обработчик замкнул и input, и lastQuery
  });
}

Ловушка: устаревшее замыкание

Обратная сторона «живой ссылки»: замыкание видит переменную такой, какая она сейчас, а не какой была при создании функции. И наоборот — если переменная была пересоздана, старое замыкание держит старую. Второй случай — знаменитый stale closure:

function startTimer(getValue) {
  setInterval(() => {
    console.log(getValue()); // всегда актуально — функция читает заново
  }, 1000);
}

// А вот так — застынет:
function startTimerBad(value) {
  setInterval(() => {
    console.log(value); // навсегда то значение, с которым вызвали
  }, 1000);
}

В React это грабли номер один в useEffect: эффект замкнул state первого рендера и «не видит» обновлений — лечится массивом зависимостей или функциональным обновлением. Подробный разбор — в статье про хуки. Классика с var в цикле из прошлой статьи — тоже устаревшее замыкание: три колбэка держат одну переменную.

Ещё один практический нюанс — память: замыкание не даёт собрать сборщиком мусора всё, на что ссылается. Обработчик, замкнувший огромный массив «на всякий случай», держит его в памяти, пока жив сам. Снимаете обработчики (removeEventListener, clearInterval) — освобождаете и их замыкания.

Проверь себя

let x = 10;
function outer() {
  let x = 20;
  return () => x;
}
const f = outer();
x = 30;
f(); // ?

Ответ: 20. Функция-стрелка замкнула x из outer (лексически ближайший), глобальный x = 30 её не касается. Ключ к любой такой задачке: найдите, где функция написана, и идите оттуда наружу.

Коротко

  • Замыкание = функция + живые ссылки на переменные области, где она создана. Не копия — ссылка.
  • Каждый вызов внешней функции создаёт новое независимое замыкание.
  • Применения: приватное состояние, фабрики функций (multiplyBy), обработчики и колбэки с контекстом, debounce/throttle.
  • Ловушка stale closure: функция держит переменную своего «поколения». В React — главная причина странностей useEffect.
  • Замыкания удерживают память: снятый обработчик освобождает всё, что замкнул.

Что почитать дальше

  • Область видимости — фундамент, без которого замыкание кажется магией.
  • Хуки: кастомные и ловушки useEffect — stale closure в боевых условиях React.
  • Прототипы — следующая тема цикла: второй механизм переиспользования в языке.