Замыкание — это функция, которая помнит
Каждый раз, когда вы объявляете функцию в JavaScript, она незаметно сохраняет связь с переменными, которые её окружают. Даже если потом эта функция выполнится где-то совсем в другом месте, она всё равно будет видеть эти переменные. Это и есть замыкание в JavaScript.
Самый короткий пример:
makeGreeter отрабатывает, возвращает внутреннюю функцию и завершается. Логично было бы предположить, что локальная переменная name тут же исчезнет — функция-то уже отработала. Но внутренняя функция всё ещё обращается к name, поэтому JavaScript держит её в памяти. greetAda помнит "Ada". greetBoris помнит "Boris". Два замыкания — два независимо сохранённых значения.
Лексическая область видимости — основа замыканий в JavaScript
За замыканиями стоит правило, которое называется лексической областью видимости: функция видит переменные того места, где она была написана, а не того, где её вызывают. Слово «лексическая» здесь значит просто «определяется по расположению в исходном коде».
show выводит "Я снаружи", а не "Я внутри caller". Функция была объявлена рядом с внешним outer — его она и видит. То, что мы вызвали её из места, где случайно есть свой outer, роли не играет.
Замыкания в JavaScript — это просто лексическая область видимости, которая переживает внешнюю функцию. Переменная не исчезает, пока на неё кто-то держит ссылку.
Каждый вызов создаёт своё замыкание
Новый вызов внешней функции создаёт новые переменные, и любая внутренняя функция, возвращённая из этого вызова, помнит именно эти переменные. Именно поэтому greetAda и greetBoris выше не конфликтовали друг с другом.
Классический пример замыкания в JS — счётчик:
a и b хранят каждый свой count. Ни один код снаружи возвращённой функции не может добраться до этих переменных — count полностью приватна. И это не какая-то специальная фича языка, которую мы включили — это естественное следствие того, как работают замыкания в JavaScript.
Приватные переменные в JavaScript без классов
Поскольку захваченные переменные доступны только через возвращённую функцию, через замыкание js можно собирать небольшие объекты с по-настоящему приватным состоянием:
balance — это не свойство возвращённого объекта, переменная живёт внутри замыкания. Добраться до неё или изменить можно только через методы, которые вы наружу отдали. Классы с приватными полями #private умеют то же самое, но замыкания появились на десятки лет раньше и до сих пор встречаются в коде повсеместно.
Классический подвох с замыканием в цикле
Чаще всего на замыканиях спотыкаются именно в циклах. Посмотрите, что творит var:
Вы наверняка ожидаете 0, 1, 2. А получаете 3, 3, 3. Вот в чём дело: var имеет функциональную область видимости, поэтому на весь цикл существует только одна переменная i. Все три замыкания захватили одну и ту же переменную, и к моменту их вызова цикл уже отработал, а i стала равна 3.
Замените var на let:
Теперь в консоли выводится 0, 1, 2. Всё дело в том, что let имеет блочную область видимости — на каждой итерации цикла создаётся новое связывание для i, и каждое замыкание захватывает свою собственную копию. Это, пожалуй, главная причина, почему let стоит предпочитать var.
Замыкания захватывают переменные, а не значения
Тонкий, но принципиальный момент: замыкание держит ссылку на саму переменную, а не на слепок её значения в момент определения функции.
printMessage читает message в момент вызова, а не в момент создания. Если нужен снимок значения — сначала скопируйте его в локальную переменную. По сути, именно это и делает let внутри цикла for.
Практический пример: функция «один раз»
Вот небольшая утилита, которая с помощью замыкания гарантирует, что функция выполнится только единожды:
called и result — это приватное состояние, которое живёт ровно столько же, сколько и возвращаемая функция. Никаких глобальных флагов, никаких лишних объектов. Этот приём — маленькая функция-помощник, приватные переменные и замыкание — одна из самых полезных вещей, которые умеет JavaScript.
Пара слов о памяти
Замыкание держит захваченные переменные живыми до тех пор, пока на само замыкание есть хоть одна ссылка. Обычно это именно то, что нужно, но есть нюанс: если повесить замыкание на что-то долгоживущее (скажем, DOM-обработчик события или глобальный кеш) и оно захватит какой-нибудь объёмный объект, сборщик мусора не сможет его освободить, пока живо замыкание.
function attach() {
const hugeData = new Array(1_000_000).fill("...");
document.addEventListener("click", () => {
console.log(hugeData.length);
});
}
Пока слушатель висит, hugeData остаётся в памяти. Уберите слушатель (или не захватывайте лишнего) — и ссылка освободится. Микроменеджмент тут не нужен, достаточно просто помнить: замыкания и память связаны напрямую.
Что важно запомнить
- Замыкание — это функция плюс переменные, которые она «видела» в момент своего объявления.
- Каждый вызов внешней функции создаёт новый набор переменных для вложенных замыканий.
- Замыкания дают приватное состояние без всяких классов.
- В циклах используйте
let— так на каждой итерации будет своя привязка. - Замыкание захватывает саму переменную, а не её значение на момент создания.
Дальше: ключевое слово this
Замыкания отвечают за переменные вокруг функции. Следующий кусок пазла — на чём именно вызвана функция. В JavaScript за это отвечает this, и ведёт он себя совсем не так, как захваченные переменные, о которых мы только что говорили.
Часто задаваемые вопросы
Что такое замыкание в JavaScript?
Замыкание — это функция, которая «запоминает» переменные из того контекста, где она была объявлена, и продолжает иметь к ним доступ даже после того, как внешняя функция уже отработала. Формально в JavaScript замыканием является любая функция, но на практике об этом говорят тогда, когда функцию возвращают наружу или передают куда-то дальше, а она всё ещё использует переменные из своего исходного окружения.
Зачем вообще нужны замыкания?
С их помощью функция несёт с собой собственное приватное состояние. Получается, что данные привязаны к функции — и при этом не нужны ни классы, ни глобальные переменные. Классические сценарии: счётчики, коллбэки, которые должны сработать один раз, мемоизация, а также небольшие модули с публичным API, где внутренняя реализация спрятана от внешнего мира.
Почему замыкания в цикле с var ведут себя странно?
Дело в том, что var имеет функциональную область видимости, поэтому все итерации делят одну и ту же переменную. Замыкания, созданные внутри цикла, ссылаются на эту единственную переменную, и к моменту их вызова она уже содержит финальное значение. Решение — использовать let: у него блочная область видимости, и на каждой итерации создаётся своя отдельная привязка.