Замыкания в JavaScript простыми словами: как они работают и зачем нужны

Замыкания в JavaScript простыми словами — это функция вместе с сохранённым лексическим окружением, то есть с «рюкзаком» значений из места, где функция была создана. Благодаря этому функция помнит переменные даже после завершения внешней функции.
Что такое лексическое окружение
Когда JavaScript выполняет функцию, он создает лексическое окружение с локальными переменными. Вложенная функция получает доступ к этим переменным и «замыкает» их на себя. После возврата внешней функции вложенная всё ещё может читать и менять те значения, пока существует хотя бы одна ссылка на вложенную функцию.
Базовый пример: счётчик
function createCounter(start) {
var count = start || 0;
return function() {
count += 1;
return count;
};
}
var counter = createCounter(5);
console.log(counter()); // 6
console.log(counter()); // 7
console.log(counter()); // 8
Функция, которую вернула createCounter
, замкнула переменную count
. Она будет жить, пока жива ссылка на counter
.
Практика: приватные данные и инкапсуляция
Замыкания — простой способ скрыть состояние без классов и модификаторов доступа.
function createWallet(initial) {
var balance = initial || 0;
return {
deposit: function(amount) {
if (amount <= 0) return false;
balance += amount;
return true;
},
withdraw: function(amount) {
if (amount <= 0 || amount > balance) return false;
balance -= amount;
return true;
},
getBalance: function() {
return balance;
}
};
}
var wallet = createWallet(100);
wallet.deposit(50);
console.log(wallet.getBalance()); // 150
console.log(wallet.balance); // undefined — приватно!
Частичное применение и конфигурирование функций
Замыкания удобны для создания «преднастроенных» функций с зафиксированными параметрами.
function makeFormatter(prefix, suffix) {
return function(value) {
return prefix + String(value) + suffix;
};
}
var asCurrency = makeFormatter('$', ' USD');
console.log(asCurrency(10)); // "$10 USD"
Memoization: ускоряем дорогие вычисления
Замыкание может хранить кэш результатов, чтобы не пересчитывать одно и то же.
function memoize(fn) {
var cache = {};
return function(key) {
if (cache.hasOwnProperty(key)) {
return cache[key];
}
var result = fn(key);
cache[key] = result;
return result;
};
}
function heavy(x) {
// некий дорогой расчёт
for (var i = 0; i < 1e7; i++) {}
return x * x;
}
var fastHeavy = memoize(heavy);
console.log(fastHeavy(9)); // считается впервые
console.log(fastHeavy(9)); // мгновенно из кэша
Типичные ошибки: замыкания в циклах и var
Классическая ловушка — использовать var
в цикле с асинхронщиной. Все колбэки видят одну и ту же переменную:
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log('var i =', i); // 3, 3, 3
}, 0);
}
Решения:
- Использовать
let
(блочная область видимости). - Или немедленно вызываемую функцию (IIFE), чтобы «зафиксировать» текущее значение.
// 1) let
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log('let i =', i); // 0, 1, 2
}, 0);
}
// 2) IIFE c var
for (var j = 0; j < 3; j++) {
(function(jCopy) {
setTimeout(function() {
console.log('IIFE j =', jCopy); // 0, 1, 2
}, 0);
})(j);
}
Замыкания и обработчики событий
Часто нужно «помнить» данные при навешивании слушателей.
function registerHandlers(buttons) {
for (var i = 0; i < buttons.length; i++) {
(function(index) {
buttons[index].addEventListener('click', function() {
console.log('Клик по кнопке #' + index);
});
})(i);
}
}
С let
код проще и безопаснее:
function registerHandlersModern(buttons) {
for (let i = 0; i < buttons.length; i++) {
buttons[i].addEventListener('click', function() {
console.log('Клик по кнопке #' + i);
});
}
}
Перформанс и утечки памяти
- Не держите лишние ссылки на функции-замыкания, если они уже не нужны — так вы удерживаете всё их окружение.
- Отписывайтесь от обработчиков событий, если DOM-элемент удалён, иначе замыкания будут удерживать память.
- Для горячих путей избегайте чрезмерной вложенности функций — профилируйте и упрощайте.
Как понять, что у вас замыкание?
Признаки:
- Вложенная функция использует переменные не из своих параметров и не из глобальной области, а из внешней функции.
- Эта внешняя функция уже завершилась, но значения продолжают «жить».
Чек-лист по применению замыканий
- Инкапсуляция состояния (счётчики, кошельки, настройки).
- Создание фабрик функций и частичное применение аргументов.
- Кэширование (memoization), debounce/throttle-обёртки.
- Навешивание обработчиков с доступом к нужному контексту данных.
Мини-практика: debounce на замыканиях
Debounce откладывает вызов функции, пока пользователь продолжает ввод. Замыкание хранит таймер.
function debounce(fn, delay) {
var timer = null;
return function() {
var ctx = this;
var args = arguments;
clearTimeout(timer);
timer = setTimeout(function() {
fn.apply(ctx, args);
}, delay);
};
}
var onInput = debounce(function(e) {
console.log('Запрос по:', e.target.value);
}, 300);
Итоги
Мы разобрали замыкания в JavaScript простыми словами, увидели их пользу в реальном коде, обсудили типичные ошибки и как их избегать. Отработайте примеры, поэкспериментируйте с var
, let
и асинхронностью — так понимание закрепится.
Хотите системно прокачать основы и уверенно писать на JavaScript? Рекомендую пройти практический курс «JavaScript: с Нуля до Гуру 2.0» — пошаговое обучение с реальными проектами.
-
-
Михаил Русаков
Комментарии (0):
Для добавления комментариев надо войти в систему.
Если Вы ещё не зарегистрированы на сайте, то сначала зарегистрируйтесь.