Замыкания в JavaScript: как они работают и зачем нужны на практике
«Замыкание — это функция вместе с окружением, в котором она была создана» — большинство разработчиков хотя бы раз слышали это определение на собеседовании, кивали и тут же забывали, потому что оно не объясняет, зачем это вообще нужно в реальном коде. Проблема в том, что замыкания обычно показывают на искусственных примерах вроде счётчика, который никто никогда не пишет в продакшене. На деле замыкания — это не отдельная фича, которую можно «использовать» или «не использовать», а естественное следствие того, как в JavaScript устроены функции и области видимости. Вы уже пользуетесь ими каждый день, даже если не называете это слово вслух. Разберёмся, что это такое без магии, и где это реально имеет значение.
Что такое замыкание на самом деле
Когда функция создаётся внутри другой функции, она получает доступ не только к своим собственным переменным, но и к переменным из внешней функции — и этот доступ сохраняется даже после того, как внешняя функция уже завершила выполнение. Вот минимальный пример:
function createCounter() {
let count = 0;
return function () {
count += 1;
return count;
};
}
const counter = createCounter();
counter(); // 1
counter(); // 2
К моменту второго вызова counter() функция createCounter давно вернула управление, и по логике обычного стекового вызова переменная count должна была исчезнуть вместе с её локальной областью видимости. Но она не исчезает — потому что внутренняя функция «замкнула» на себе ссылку на эту переменную, и движок JavaScript держит её в памяти, пока жива хоть одна функция, которая может к ней обратиться. Это и есть замыкание: не копия значения, а живая ссылка на конкретную переменную в конкретной области видимости.
Где замыкания реально нужны в коде
Как только заменить абстрактный «счётчик» на реальные задачи, становится видно, что замыкания — это рабочая лошадка, а не теоретическая конструкция:
- Приватное состояние без классов. Переменная, объявленная внутри функции, недоступна снаружи напрямую — к ней можно обратиться только через функции, которые возвращает создающая функция. Это простой способ получить инкапсуляцию там, где полноценный класс избыточен.
- Debounce и throttle. Функции вроде debounce для поля поиска хранят в замыкании таймер и время последнего вызова между отдельными срабатываниями обработчика событий — без этого пришлось бы городить состояние снаружи.
- Мемоизация. Кэш результатов вычислений обычно живёт в объекте или Map, захваченном замыканием, и переживает между вызовами обёрнутой функции, оставаясь недоступным для остального кода.
- Фабрики функций с параметрами. Функция, которая принимает конфигурацию и возвращает другую, уже настроенную под неё функцию — частый паттерн в middleware Express, обработчиках событий с дополнительным контекстом, генераторах валидаторов.
Во всех этих случаях замыкание — не самоцель, а побочный эффект того, что вам нужно состояние, которое живёт дольше одного вызова функции, но не должно быть видно снаружи.
Частая ловушка: замыкания внутри циклов
Самый известный баг, связанный с замыканиями, возникает в циклах, когда для переменной цикла используется var:
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// выведет: 3, 3, 3
Причина в том, что var не создаёт отдельную переменную на каждую итерацию — она одна на весь цикл, и все три замыкания ссылаются на одну и ту же ячейку памяти. К моменту, когда сработает первый setTimeout, цикл уже закончился, и i равно 3. Замена var на let решает проблему, потому что let создаёт новую привязку переменной на каждую итерацию цикла — и каждое замыкание получает собственную, независимую копию:
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// выведет: 0, 1, 2
Замыкание — это не отдельный синтаксис, который нужно специально применять, а естественное поведение функций в JavaScript. Понимать его стоит не для того, чтобы «использовать замыкания», а чтобы не удивляться, когда переменная из внешней функции внезапно оказывается общей на несколько вызовов сразу.
На что обратить внимание
У замыканий есть и обратная сторона, о которой реже говорят. Раз замкнутая переменная держится в памяти, пока жива хотя бы одна ссылающаяся на неё функция, долгоживущие замыкания на большие объекты — например, DOM-узлы или большие массивы, случайно захваченные вместе с нужной переменной, — могут мешать сборщику мусора освободить память вовремя. Обычно это не проблема в коротких обработчиках, но становится заметно в длинных сессиях одностраничных приложений, где обработчики событий накапливаются, а старые версии данных никто явно не отписывает.
Практический ориентир простой: если функция возвращает или сохраняет другую функцию, стоит на секунду задуматься, какие переменные из внешней области видимости она на самом деле «утаскивает» с собой, и не тянется ли туда что-то тяжелее, чем нужно.
🎬 Короткое видео по теме статьи: «Замыкания в JavaScript» на YouTube Shorts.
← Все статьи