Что такое замыкание
Хороший и простой пример с решением проблемы — это лучший способ объяснения сложной теории. Поэтому предлагаю разобрать самую популярную задачу, которую используют для объяснения замыканий — счетчик.
Нам нужно создать функцию counter, которая при каждом своем вызове будет возвращать числовое значение, увеличенное на единицу. То есть будет считать свои вызовы.
Сейчас пишем код без использования замыкания. Первое, что приходит в голову — использовать глобальную переменную count. Давайте так и сделаем:
let count = 0; // Глобальная переменная — наша "память"
function counter() {
count = count + 1;
console.log("Счет:", count);
}
counter();
counter();
counter();

Можем себя поздравить: задача решена и все работает. Но так ли этот способ хорош? У него есть ряд неприятных проблем.
Уязвимость данных. Переменная count видна всему коду. Любая другая функция или строка кода может случайно (или намеренно) изменить ее значение: count = 0;, count = «Упс!»;. Наш счетчик сломается.
Конфликты имен. В большом проекте легко объявить еще одну глобальную переменную с именем count для других целей. Две части кода начнут мешать друг другу, и найти причину ошибки будет сложно.
Нет изоляции. Мы не можем создать два независимых счетчика. У нас всего одна общая переменная на всех.
И тут на помощь приходит замыкание. Оно дает нам ту же память, но спрятанную и защищенную. Сначала предлагаю посмотреть код, а затем я разберу, как он работает:
function createCounter() {
let count = 0; // Эта переменная — наша будущая память
// Возвращаем НОВУЮ функцию, которая будет работать с этой памятью
return function counter() {
count = count + 1;
console.log("Счет:", count);
};
}
console.log("Первый счетчик");
// Создаем "экземпляр" счетчика
const myCounter = createCounter();
// Проверяем
myCounter();
myCounter();
myCounter();
console.log("Второй счетчик");
// Создаем второй "экземпляр" счетчика
const myCounter2 = createCounter();
// Проверяем
myCounter2();
myCounter2();

А теперь пошагово разберемся, как все это работает:
- Объявляем функцию
createCounter(). При ее вызове создается локальная переменнаяcount = 0;
createCounter()возвращает новую функциюcounter. Функция видит переменную count из того места, где она была создана (из области видимостиcreateCounter);
- Сохраняем возвращенную функцию в переменную
myCounter. Теперь, даже после того какcreateCounter()завершила свою работу, ее внутренняя переменная count не уничтожается. Она остается привязанной или замкнутой на возвращенной функцииmyCounter.
- Обращаемся к одной и той же запомненной переменной count каждый раз, как вызываем
myCounter(). Увеличиваем ее и выводим значение. Доступ к этой переменной есть только у функции myCounter. Она стала приватной.
С примером разобрались, а теперь перейдем к самому определению замыкания.
Замыкание — это способность функции запоминать и иметь доступ к переменным из своей внешней (лексической) области видимости даже после того, как внешняя функция завершила выполнение.
Итак, мы решили одну и ту же задачу двумя путями. Первый способ с глобальной переменной, простой, но ненадежный. Второй способ с замыканием, чуть сложнее для понимания, но он дает нам:
- безопасность — данные защищены;
- гибкость — мы можем создать множество независимых счетчиков, просто вызывая
createCounter()несколько раз.
Мы не использовали никакого специального синтаксиса, а просто воспользовались тем, как в JavaScript работают области видимости и функции. В этом и есть красота замыканий. Это не отдельная фича, а естественное следствие устройства языка, которое мы можем обратить себе на пользу.
Интересный пример
Предлагаю решить чуть более сложную, но очень жизненную задачу. Допустим, у нас есть функция, которая выполняет тяжелые вычисления. Если мы вызываем ее много раз с одними и теми же аргументами, она каждый раз заново проводит все расчеты, тратя время и ресурсы.
Как это исправить? Научим функцию запоминать (мемоизировать) результаты своих прошлых вызовов. Для создания такой памяти отлично подходит замыкание.
Создадим функцию, которая имитирует сложное вычисление. Чтобы увидеть разницу, мы добавим искусственную задержку. Мы хотим, чтобы при повторном вызове с теми же аргументами функция не вычисляла все заново, а мгновенно возвращала сохраненный ранее результат:
function createMemoized() {
const cache = {}; // Пустой объект - наша память, замкнутая внутри
// Возвращаем основную функцию, которая будет использовать cache
return function heavyCalculation(x) {
// Перед вычислением проверяем: а нет ли уже ответа в кэше?
if (cache[x] !== undefined) {
console.log(`Взято из кэша для ${x}:`, cache[x]);
return cache[x];
}
// ИМИТИРУЕМ СЛОЖНОЕ И ДОЛГОЕ ВЫЧИСЛЕНИЕ
console.log(`Начинаю долгий расчет для ${x}`);
// Искусственная задержка (в реальности здесь могла бы быть
// сложная математика, запрос к API или обработка большого массива)
const startTime = Date.now();
while (Date.now() - startTime < 2000) {
// Просто ждем 2 секунды, имитируя нагрузку
}
// Само "тяжелое" вычисление (например, возведение в степень)
const result = Math.pow(x, 3);
// Сохраняем результат в кэш, чтобы не считать в следующий раз
cache[x] = result;
console.log(`Вычисление завершено! Запомнил результат для ${x}:`, result);
return result;
};
}
// Создаем нашу "умную" функцию с памятью
const memoizedHeavyCalc = createMemoized();
// Проверяем в действии
console.log("Первый вызов с аргументом 5:");
console.log("Результат:", memoizedHeavyCalc(5)); // Долгий расчет
console.log("Второй вызов с тем же аргументом 5:");
console.log("Результат:", memoizedHeavyCalc(5)); // Мгновенно из кэша!
console.log("Первый вызов с аргументом 3:");
console.log("Результат:", memoizedHeavyCalc(3)); // Снова долгий расчет
console.log("Еще раз с аргументом 5:");
console.log("Результат:", memoizedHeavyCalc(5)); // Снова мгновенно!

Больше подобных материалов по JS:
Да, кода много, и он может показаться громоздким. Но если его внимательно изучить, то в целом все работает достаточно просто. Вся суть в объекте cache, который является хранилищем нашей возвращаемой функции. Функция о нем помнит благодаря замыканию.
Подробнее, как работать со свойствами объектов разными способами, я уже ранее рассказывал в этой статье.
Пример немного надуманный, но при желании вы можете его расширить, добавив свой расчет или даже передавая через параметр другую функцию, что придаст гибкости.
Кстати, раз уж сказал про мемоизацию, то дать простое определение, точно будет полезно:
Мемоизация — это техника оптимизации, используемая для ускорения работы программ. Работает с помощью сохранения результатов дорогостоящих (долгих или ресурсоемких) вызовов функций и возврата кэшированного результата, когда те же входные данные встречаются снова.
В итоге
Замыкания на практике не какая-то надстройка над языком, а естественное следствие того, как JavaScript работает с областями видимости. Функция просто «помнит» окружение, в котором была создана, и может использовать его переменные даже после завершения внешней функции.
Теперь вы понимаете, что замыкания — это не отдельная фича, а фундаментальный механизм языка. Вы встретите их везде: в обработчиках событий, фабриках функций, модулях. И самое важное — осознать эту невидимую связь между функцией и ее памятью.