Антон Ларичев

Введение
Замыкания — одна из тех концепций JavaScript, которая пугает новичков, но становится незаменимым инструментом в руках опытного разработчика. В этой статье разберём, что такое замыкание, как оно работает «под капотом» и когда его использовать на практике.
Что такое замыкание
Замыкание (closure) — это функция, которая «помнит» переменные из своей внешней области видимости даже после того, как внешняя функция завершила выполнение.
Звучит сложно? Давайте на примере:
function создатьСчётчик() {
// переменная живёт во внешней области видимости
let счёт = 0;
return function() {
счёт += 1;
return счёт;
};
}
const счётчик = создатьСчётчик();
console.log(счётчик()); // 1
console.log(счётчик()); // 2
console.log(счётчик()); // 3
Функция, которую возвращает создатьСчётчик, «захватывает» переменную счёт. Каждый вызов увеличивает её значение, и оно сохраняется между вызовами — хотя создатьСчётчик давно отработала.
Как работает область видимости
Чтобы понять замыкания, нужно разобраться с областями видимости (scope).
В JavaScript каждая функция создаёт свою область видимости. Переменные, объявленные внутри функции, недоступны снаружи. Но вложенная функция видит переменные родительской:
function внешняя() {
const сообщение = 'Привет';
function внутренняя() {
// внутренняя видит переменные родителя
console.log(сообщение);
}
внутренняя();
}
внешняя(); // Привет
Цепочка областей видимости называется scope chain. Когда JavaScript ищет переменную, он проверяет текущую область, затем родительскую, и так вплоть до глобальной. Замыкание — это сохранение этой цепочки после выхода из функции.
Замыкание на практике
Инкапсуляция данных
Замыкания позволяют скрыть данные от внешнего мира — аналог приватных полей класса:
function создатьКошелёк(начальныйБаланс) {
let баланс = начальныйБаланс;
return {
пополнить(сумма) {
баланс += сумма;
console.log(`Баланс: ${баланс}`);
},
снять(сумма) {
if (сумма > баланс) {
console.log('Недостаточно средств');
return;
}
баланс -= сумма;
console.log(`Баланс: ${баланс}`);
},
проверить() {
return баланс;
}
};
}
const кошелёк = создатьКошелёк(1000);
кошелёк.пополнить(500); // Баланс: 1500
кошелёк.снять(200); // Баланс: 1300
// прямого доступа к переменной баланс нет — только через методы
Функции-фабрики
Замыкания удобны для создания похожих функций с разными начальными параметрами:
function создатьМножитель(множитель) {
// множитель захватывается в замыкание
return function(число) {
return число * множитель;
};
}
const удвоить = создатьМножитель(2);
const утроить = создатьМножитель(3);
console.log(удвоить(5)); // 10
console.log(утроить(5)); // 15
Мемоизация
Замыкание может хранить кэш результатов дорогостоящих вычислений:
function мемоизировать(fn) {
const кэш = {}; // кэш живёт в замыкании
return function(аргумент) {
if (кэш[аргумент] !== undefined) {
console.log('Из кэша');
return кэш[аргумент];
}
кэш[аргумент] = fn(аргумент);
return кэш[аргумент];
};
}
const кешированноеВозведение = мемоизировать((n) => n * n);
console.log(кешированноеВозведение(4)); // 16
console.log(кешированноеВозведение(4)); // Из кэша → 16
Частые ошибки
Замыкание в цикле с var
Классическая ловушка для новичков:
// Неправильно: все функции захватывают одну и ту же переменную i
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // выведет 3, 3, 3
}, 1000);
}
// Правильно: let создаёт новую переменную для каждой итерации
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // выведет 0, 1, 2
}, 1000);
}
Проблема с var в том, что он не создаёт новую переменную на каждой итерации — все колбэки ссылаются на одну i. К моменту срабатывания таймаута цикл завершился, и i равно 3. let решает проблему, создавая отдельный экземпляр для каждой итерации.
Утечки памяти
Замыкания удерживают в памяти всю цепочку областей видимости. Если замыкание живёт долго, а родительская область содержит тяжёлые данные — это утечка:
function проблемнаяФункция() {
// тяжёлые данные в родительской области
const большойМассив = new Array(1000000).fill('данные');
return function() {
// замыкание удерживает большойМассив в памяти, хотя не использует
return 'Привет';
};
}
const утечка = проблемнаяФункция();
// большойМассив не будет очищен сборщиком мусора
Решение: не объявляйте тяжёлые данные там, где они не нужны замыканию, или обнуляйте переменную явно после использования.
Неожиданное разделение состояния
function создатьПриветствие(имя) {
let сообщение = `Привет, ${имя}!`;
return {
показать() {
console.log(сообщение);
},
изменить(новоеИмя) {
// все методы разделяют одну захваченную переменную
сообщение = `Привет, ${новоеИмя}!`;
}
};
}
const привет = создатьПриветствие('Иван');
привет.показать(); // Привет, Иван!
привет.изменить('Мария');
привет.показать(); // Привет, Мария!
Это не всегда ошибка, но важно помнить: все методы объекта, созданного через замыкание, разделяют одно и то же захваченное состояние.
Заключение
Замыкания — фундаментальная концепция JavaScript, которая лежит в основе множества паттернов: модульного кода, каррирования, мемоизации и реактивного программирования. Понимание того, как функции захватывают переменные из внешних областей видимости, открывает доступ к более чистому и выразительному коду.
Главное запомнить: замыкание — это не магия. Это функция, которая помнит среду, в которой была создана. Начните применять их в небольших задачах — счётчики, фабрики, кэширование — и вы быстро почувствуете их силу.






Комментарии
0