Что такое замыкание и как оно создаётся
Что такое замыкание и как оно создаётся
Представьте: вы пишете компонент на React, добавляете обработчик события внутри useEffect, и вдруг замечаете, что он читает устаревшее значение стейта. Или создаёте несколько кнопок в цикле, и все они ведут себя одинаково — хотя должны делать разное. За обоими сценариями стоит одна и та же механика: замыкание (closure). Понять его — значит перестать бороться с JavaScript и начать использовать его в свою пользу.
Функция внутри функции — вот где всё начинается
Замыкание возникает, когда функция «запоминает» переменные из того места, где она была создана, — даже если это место уже завершило выполнение. Звучит абстрактно, поэтому сразу к коду.
function makeGreeting(name) {
// переменная name живёт в области видимости makeGreeting
return function () {
console.log(`Привет, ${name}!`);
};
}
const greetAlice = makeGreeting('Alice');
greetAlice(); // "Привет, Alice!"
makeGreeting уже завершила работу, но возвращённая функция всё равно знает, кто такой name. Она не скопировала значение — она держит живую ссылку на лексическое окружение (lexical environment) родительской функции. Это и есть замыкание.
Замыкание — это функция вместе с лексическим окружением, в котором она была создана.
Как JavaScript хранит переменные: лексическое окружение
Каждый раз, когда вызывается функция, JavaScript создаёт для неё лексическое окружение — внутреннюю структуру данных, которая хранит все локальные переменные этого вызова. Когда функция завершается, это окружение обычно уничтожается сборщиком мусора. Но если внутри была создана другая функция, которая ссылается на переменные родителя — окружение остаётся живым, пока жива внутренняя функция.
Вот как это выглядит пошагово:
- Вызывается
makeGreeting('Alice')— создаётся лексическое окружение сname = 'Alice'. - Внутри создаётся анонимная функция, которая захватывает это окружение.
makeGreetingвозвращает анонимную функцию и завершает работу.- Лексическое окружение с
nameне уничтожается, потому что на него ссылается возвращённая функция. - При вызове
greetAlice()функция находитnameв захваченном окружении.
Именно поэтому каждый вызов makeGreeting создаёт независимое замыкание:
const greetAlice = makeGreeting('Alice');
const greetBob = makeGreeting('Bob');
greetAlice(); // "Привет, Alice!"
greetBob(); // "Привет, Bob!"
greetAlice и greetBob — две разные функции с двумя разными лексическими окружениями. Они не делят переменную name между собой.
Три условия, при которых возникает замыкание
Замыкание создаётся автоматически — вам не нужно ничего специально делать. Оно появляется всегда, когда выполняются три условия:
- Есть внешняя функция с локальными переменными.
- Внутри неё создаётся другая функция.
- Внутренняя функция обращается к переменным внешней.
Это не экзотика — это происходит в вашем коде постоянно. Каждый колбэк в addEventListener, каждый обработчик в useCallback, каждая функция внутри другой функции — потенциальное замыкание.
Замыкание — это не копия, а ссылка
Здесь кроется важный нюанс, который часто путает разработчиков. Замыкание захватывает не значение переменной в момент создания, а саму переменную — живую ссылку на ячейку памяти. Это означает, что если переменная изменится, замыкание увидит новое значение.
function makeCounter() {
let count = 0;
return {
increment() { count++; },
get() { return count; },
};
}
const counter = makeCounter();
counter.increment();
counter.increment();
console.log(counter.get()); // 2
Обе функции — increment и get — замкнуты на одну и ту же переменную count. Когда increment меняет count, get видит изменение. Это не баг, это фича: замыкания позволяют нескольким функциям разделять общее состояние.
Замыкание без вложенных функций — миф
Иногда можно услышать, что замыкание — это «функция, возвращающая функцию». Это неточно. Замыкание создаётся в момент определения внутренней функции, а не в момент возврата. Вот пример, где функция никуда не возвращается, но замыкание всё равно работает:
function setup() {
let message = 'Готово';
setTimeout(function () {
console.log(message); // "Готово" — через 1 секунду
}, 1000);
}
setup();
Колбэк setTimeout замкнут на message. setup давно завершилась, но когда через секунду сработает таймер — message будет доступна. Именно поэтому в React-компонентах обработчики событий видят пропсы и стейт: они замкнуты на них в момент рендера.
Почему это важно для React-разработчика
В React каждый рендер — это новый вызов функции-компонента. Каждый раз создаются новые замыкания, которые захватывают актуальные значения пропсов и стейта на момент этого рендера. Вот почему useCallback и useMemo существуют: они позволяют контролировать, какие замыкания пересоздаются, а какие — нет.
function SearchComponent({ query }) {
// Каждый рендер создаёт новое замыкание с актуальным query
const handleSearch = () => {
console.log('Ищем:', query);
};
return <button onClick={handleSearch}>Найти</button>;
}
Без понимания замыканий поведение useEffect с зависимостями, «устаревшие» значения в обработчиках и бесконечные циклы ре-рендеров выглядят как магия. С пониманием — это предсказуемая механика.
Замыкание — не абстрактная концепция из учебника. Это инструмент, который JavaScript использует под капотом каждый раз, когда вы пишете функцию внутри функции. Следующий шаг — разобраться, как именно работают области видимости и что значит «захватить переменную».