Замыкания в JavaScript: как и зачем
Содержание
О проверке кода. Примеры не запускались — в отличие от материалов по Node.js с фактическим выводом. Говорим прямо, а не ставим бейдж «проверено».
Замыкания — тема, которая пугает новичков и обязательно всплывает на собеседовании. На деле это простая идея, естественно вытекающая из того, как JavaScript ищет переменные.
Что это на самом деле
Замыкание — это функция, которая помнит, где она была создана, и продолжает видеть тамошние переменные.
function makeCounter() {
let count = 0; // переменная внешней функции
return function () {
count++; // внутренняя функция ВИДИТ count
return count;
};
}
const counter = makeCounter();
counter(); // 1
counter(); // 2
counter(); // 3
Смотрите, что произошло: makeCounter уже завершилась, её переменная count по всем правилам должна была исчезнуть. Но возвращённая функция помнит count и продолжает им пользоваться. Каждый вызов увеличивает то самое count.
Это и есть замыкание: внутренняя функция «замкнула» вокруг себя переменные из места, где родилась, и унесла их с собой.
Почему так работает
Никакой магии — это прямое следствие лексической области видимости. Когда функция объявляется, она запоминает окружение, в котором создана. Обращаясь к переменной, она ищет её сначала у себя, потом во внешней функции, потом ещё выше — по цепочке областей.
Эта цепочка не рвётся, когда внешняя функция завершается. Пока живёт внутренняя функция, живут и захваченные ею переменные — сборщик мусора их не трогает.
-
1
Внешняя функция создаёт переменную
let count = 0внутриmakeCounter -
2
Возвращает внутреннюю функциюТа ссылается на
countиз внешней -
3
Внешняя завершаетсяНо
countНЕ исчезает — на неё ссылаются -
4
Внутренняя живёт и помнит
countКаждый вызов работает с той же переменной
Главное применение: приватные данные
В JavaScript до недавнего времени не было приватных полей, и замыкания их заменяли. Переменная внутри функции недоступна снаружи — достать её можно только через методы, которые вы сами открыли:
function createAccount(initial) {
let balance = initial; // приватно! снаружи не достать
return {
deposit(sum) { balance += sum; },
withdraw(sum) { balance -= sum; },
getBalance() { return balance; },
};
}
const acc = createAccount(100);
acc.deposit(50);
acc.getBalance(); // 150
acc.balance; // undefined — нет прямого доступа!
balance существует, но добраться до него можно только через deposit, withdraw, getBalance. Никто не изменит баланс напрямую в обход правил. Это инкапсуляция через замыкание — тот же принцип, что приватные поля # в классах, только старше.
Классическая ловушка: var в цикле
Самый частый способ обжечься о замыкания:
// ❌ напечатает 3, 3, 3
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i));
}
// ✅ напечатает 0, 1, 2
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i));
}
С var переменная i одна на весь цикл, и все три замыкания ссылаются на неё же. Когда таймеры срабатывают, цикл давно кончился, и i равна 3 — все печатают 3.
С let на каждой итерации создаётся новая i, и каждое замыкание захватывает своё значение. Это и есть главная практическая причина, по которой let вытеснил var.
Реальные применения
Замыкания — не академическая экзотика, они повсюду:
- Хуки React —
useStateвозвращает функцию, которая через замыкание помнит, к какому кусочку состояния относится; - Debounce и throttle — таймер и последнее значение хранятся в замыкании между вызовами;
- Фабрики функций —
makeCounter, генераторы конфигураций, каррирование; - Обработчики событий — колбэк помнит переменные из места, где его повесили;
- Мемоизация — кэш результатов живёт в замыкании.
Если вы пишете современный JavaScript, вы используете замыкания постоянно, даже не называя их так.
Про память
Честная оговорка. Замыкание держит захваченные переменные живыми, пока живёт само. Обычно это ровно то, что нужно. Но если замыкание случайно захватило большой объект и живёт долго (висит обработчиком, лежит в кэше), этот объект не освободится — потенциальная утечка памяти.
Практическое правило: не захватывайте больше, чем нужно. Если функции нужно одно поле большого объекта — передайте это поле, а не весь объект.
Смежные темы: область видимости и hoisting — фундамент замыканий; переменные — почему let, а не var; функции. Полный список — в уроках JavaScript.