Замыкание (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.
- Прототипы — следующая тема цикла: второй механизм переиспользования в языке.