1. Что такое замыкание и как оно создаётся
Что такое замыкание и как оно создаётся
Представьте: вы пишете компонент на React, добавляете обработчик события внутри useEffect, и вдруг замечаете, что он читает устаревшее значение стейта. Или создаёте несколько кнопок в цикле, и все они ведут себя одинаково — хотя должны делать разное. За обоими сценариями стоит одна и та же механика: замыкание (closure). Понять его — значит перестать бороться с JavaScript и начать использовать его в свою пользу.
Функция внутри функции — вот где всё начинается
Замыкание возникает, когда функция «запоминает» переменные из того места, где она была создана, — даже если это место уже завершило выполнение. Звучит абстрактно, поэтому сразу к коду.
makeGreeting уже завершила работу, но возвращённая функция всё равно знает, кто такой name. Она не скопировала значение — она держит живую ссылку на лексическое окружение (lexical environment) родительской функции. Это и есть замыкание.
> Замыкание — это функция вместе с лексическим окружением, в котором она была создана.
Как JavaScript хранит переменные: лексическое окружение
Каждый раз, когда вызывается функция, JavaScript создаёт для неё лексическое окружение — внутреннюю структуру данных, которая хранит все локальные переменные этого вызова. Когда функция завершается, это окружение обычно уничтожается сборщиком мусора. Но если внутри была создана другая функция, которая ссылается на переменные родителя — окружение остаётся живым, пока жива внутренняя функция.
Вот как это выглядит пошагово:
makeGreeting('Alice') — создаётся лексическое окружение с name = 'Alice'.makeGreeting возвращает анонимную функцию и завершает работу.name не уничтожается, потому что на него ссылается возвращённая функция.greetAlice() функция находит name в захваченном окружении.!Схема работы замыкания: лексическое окружение родительской функции остаётся живым
Именно поэтому каждый вызов makeGreeting создаёт независимое замыкание:
greetAlice и greetBob — две разные функции с двумя разными лексическими окружениями. Они не делят переменную name между собой.
Три условия, при которых возникает замыкание
Замыкание создаётся автоматически — вам не нужно ничего специально делать. Оно появляется всегда, когда выполняются три условия:
Это не экзотика — это происходит в вашем коде постоянно. Каждый колбэк в addEventListener, каждый обработчик в useCallback, каждая функция внутри другой функции — потенциальное замыкание.
Замыкание — это не копия, а ссылка
Здесь кроется важный нюанс, который часто путает разработчиков. Замыкание захватывает не значение переменной в момент создания, а саму переменную — живую ссылку на ячейку памяти. Это означает, что если переменная изменится, замыкание увидит новое значение.
Обе функции — increment и get — замкнуты на одну и ту же переменную count. Когда increment меняет count, get видит изменение. Это не баг, это фича: замыкания позволяют нескольким функциям разделять общее состояние.
!Интерактивная демонстрация замыкания: как функции разделяют переменную count
Замыкание без вложенных функций — миф
Иногда можно услышать, что замыкание — это «функция, возвращающая функцию». Это неточно. Замыкание создаётся в момент определения внутренней функции, а не в момент возврата. Вот пример, где функция никуда не возвращается, но замыкание всё равно работает:
Колбэк setTimeout замкнут на message. setup давно завершилась, но когда через секунду сработает таймер — message будет доступна. Именно поэтому в React-компонентах обработчики событий видят пропсы и стейт: они замкнуты на них в момент рендера.
Почему это важно для React-разработчика
В React каждый рендер — это новый вызов функции-компонента. Каждый раз создаются новые замыкания, которые захватывают актуальные значения пропсов и стейта на момент этого рендера. Вот почему useCallback и useMemo существуют: они позволяют контролировать, какие замыкания пересоздаются, а какие — нет.
Без понимания замыканий поведение useEffect с зависимостями, «устаревшие» значения в обработчиках и бесконечные циклы ре-рендеров выглядят как магия. С пониманием — это предсказуемая механика.
Замыкание — не абстрактная концепция из учебника. Это инструмент, который JavaScript использует под капотом каждый раз, когда вы пишете функцию внутри функции. Следующий шаг — разобраться, как именно работают области видимости и что значит «захватить переменную».