Замыкание (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
});
}
Немедленный вызов (IIFE). До ES-модулей у файла не было своей области: всё, что объявлено снаружи функций, попадало в глобальную. Поэтому модуль заворачивали в функцию и тут же вызывали её — Immediately Invoked Function Expression. Переменные внутри живут в замыкании, наружу выходит только то, что функция вернула:
живой пример
const counter = (function () {
let count = 0;
return { next: () => ++count };
})();
console.log(counter.next());
console.log(counter.next());
console.log(typeof count);
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Сегодня свою область даёт каждый модуль, и IIFE встречается в старом коде и в одном частом случае: (async () => { ... })(), чтобы использовать await в CommonJS-файле, где await на верхнем уровне нельзя. Грабля — строка перед IIFE без точки с запятой: скобка продолжит предыдущее выражение и попытается вызвать его как функцию.
живой пример
// ошибка: без точки с запятой скобка вызывает число 1 как функцию
const total = 1
(function () {
console.log("не выполнится");
})();
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Ловушка: устаревшее замыканиеспросят на собеседовании
Обратная сторона «живой ссылки»: замыкание видит переменную такой, какая она сейчас, а не какой была при создании функции. И наоборот — если переменная была пересоздана, старое замыкание держит старую. Второй случай — знаменитый stale closure:
function startTimer(getValue) {
setInterval(() => {
console.log(getValue()); // всегда актуально — функция читает заново
}, 1000);
}
// А вот так — застынет:
function startTimerBad(value) {
setInterval(() => {
console.log(value); // навсегда то значение, с которым вызвали
}, 1000);
}
В React это грабли номер один в useEffect: эффект замкнул state первого рендера и «не видит» обновлений — лечится массивом зависимостей или функциональным обновлением. Подробный разбор — в статье про хуки. На сервере та же ловушка выглядит иначе: обработчик очереди замкнул config, прочитанный при старте, и не видит настроек, перечитанных после SIGHUP, — лечится так же, функцией-геттером вместо значения. Классика с 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.
- Прототипы — следующая тема цикла: второй механизм переиспользования в языке.