Menu
Русский

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

Замыкание — это функция, которая помнит переменные из своего окружения. Разбираем, как работают замыкания в JavaScript, на живых примерах и реальных задачах.

Замыкание — это функция, которая помнит

Каждый раз, когда вы объявляете функцию в JavaScript, она незаметно сохраняет связь с переменными, которые её окружают. Даже если потом эта функция выполнится где-то совсем в другом месте, она всё равно будет видеть эти переменные. Это и есть замыкание в JavaScript.

Самый короткий пример:

index.js
Output
Click Run to see the output here.

makeGreeter отрабатывает, возвращает внутреннюю функцию и завершается. Логично было бы предположить, что локальная переменная name тут же исчезнет — функция-то уже отработала. Но внутренняя функция всё ещё обращается к name, поэтому JavaScript держит её в памяти. greetAda помнит "Ada". greetBoris помнит "Boris". Два замыкания — два независимо сохранённых значения.

Лексическая область видимости — основа замыканий в JavaScript

За замыканиями стоит правило, которое называется лексической областью видимости: функция видит переменные того места, где она была написана, а не того, где её вызывают. Слово «лексическая» здесь значит просто «определяется по расположению в исходном коде».

index.js
Output
Click Run to see the output here.

show выводит "Я снаружи", а не "Я внутри caller". Функция была объявлена рядом с внешним outer — его она и видит. То, что мы вызвали её из места, где случайно есть свой outer, роли не играет.

Замыкания в JavaScript — это просто лексическая область видимости, которая переживает внешнюю функцию. Переменная не исчезает, пока на неё кто-то держит ссылку.

Каждый вызов создаёт своё замыкание

Новый вызов внешней функции создаёт новые переменные, и любая внутренняя функция, возвращённая из этого вызова, помнит именно эти переменные. Именно поэтому greetAda и greetBoris выше не конфликтовали друг с другом.

Классический пример замыкания в JS — счётчик:

index.js
Output
Click Run to see the output here.

a и b хранят каждый свой count. Ни один код снаружи возвращённой функции не может добраться до этих переменных — count полностью приватна. И это не какая-то специальная фича языка, которую мы включили — это естественное следствие того, как работают замыкания в JavaScript.

Приватные переменные в JavaScript без классов

Поскольку захваченные переменные доступны только через возвращённую функцию, через замыкание js можно собирать небольшие объекты с по-настоящему приватным состоянием:

index.js
Output
Click Run to see the output here.

balance — это не свойство возвращённого объекта, переменная живёт внутри замыкания. Добраться до неё или изменить можно только через методы, которые вы наружу отдали. Классы с приватными полями #private умеют то же самое, но замыкания появились на десятки лет раньше и до сих пор встречаются в коде повсеместно.

Классический подвох с замыканием в цикле

Чаще всего на замыканиях спотыкаются именно в циклах. Посмотрите, что творит var:

index.js
Output
Click Run to see the output here.

Вы наверняка ожидаете 0, 1, 2. А получаете 3, 3, 3. Вот в чём дело: var имеет функциональную область видимости, поэтому на весь цикл существует только одна переменная i. Все три замыкания захватили одну и ту же переменную, и к моменту их вызова цикл уже отработал, а i стала равна 3.

Замените var на let:

index.js
Output
Click Run to see the output here.

Теперь в консоли выводится 0, 1, 2. Всё дело в том, что let имеет блочную область видимости — на каждой итерации цикла создаётся новое связывание для i, и каждое замыкание захватывает свою собственную копию. Это, пожалуй, главная причина, почему let стоит предпочитать var.

Замыкания захватывают переменные, а не значения

Тонкий, но принципиальный момент: замыкание держит ссылку на саму переменную, а не на слепок её значения в момент определения функции.

index.js
Output
Click Run to see the output here.

printMessage читает message в момент вызова, а не в момент создания. Если нужен снимок значения — сначала скопируйте его в локальную переменную. По сути, именно это и делает let внутри цикла for.

Практический пример: функция «один раз»

Вот небольшая утилита, которая с помощью замыкания гарантирует, что функция выполнится только единожды:

index.js
Output
Click Run to see the output here.

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: у него блочная область видимости, и на каждой итерации создаётся своя отдельная привязка.

Учитесь программировать с Coddy

НАЧАТЬ