Как работают замыкания (closures) в JavaScript? Объясните на примерах
Что такое замыкание и как оно работает
Замыкание (closure) — это комбинация функции и её лексического окружения (scope), в котором эта функция была создана. Простыми словами: функция "запоминает" переменные из внешней функции, даже после того, как внешняя функция завершила выполнение.
Разбор вашего примера
Давайте разберём, что происходит в коде шаг за шагом:
function createCounter() {
let count = 0; // (1) Локальная переменная
return function() { // (2) Внутренняя функция
count++;
return count;
};
}
const counter1 = createCounter(); // (3)
const counter2 = createCounter(); // (4)
Шаг 1: Когда вызывается createCounter(), создаётся новое лексическое окружение (назовём его Env1). В нём объявляется переменная count = 0.
Шаг 2: Внутренняя функция (анонимная) создаётся внутри Env1. Она получает скрытое свойство [[Environment]], которое ссылается на Env1. Это и есть механизм замыкания — функция "запоминает" окружение, в котором родилась.
Шаг 3: counter1 получает ссылку на эту внутреннюю функцию. Внешняя функция createCounter завершает работу, но Env1 не уничтожается, потому что на него всё ещё ссылается counter1 через [[Environment]].
Шаг 4: При вызове counter1() создаётся новое временное окружение для этого вызова. JavaScript ищет переменную count: сначала в локальном окружении (не находит), затем переходит в [[Environment]] — то есть в Env1 — и находит её там. count увеличивается до 1.
Шаг 5: При вызове createCounter() во второй раз (для counter2) создаётся совершенно новое лексическое окружение Env2 с новой переменной count = 0. counter2 ссылается на Env2, а не на Env1.
Поэтому counter1 и counter2 — это независимые счётчики, каждый со своим "личным" count.
Как JavaScript "помнит" переменные?
Ответ: через скрытое свойство [[Environment]]. Когда функция создаётся, движок JavaScript сохраняет в этом свойстве ссылку на текущее лексическое окружение. При вызове функции движок использует эту ссылку для поиска переменных, которые не найдены в локальном окружении.
Практические примеры использования замыканий
1. Приватные переменные (инкапсуляция)
function createUser(name) {
let _password = '';
return {
getName: () => name,
setPassword: (pwd) => { _password = pwd; },
checkPassword: (pwd) => _password === pwd
};
}
const user = createUser('Alice');
user.setPassword('12345');
console.log(user.checkPassword('12345')); // true
console.log(user._password); // undefined — нет прямого доступа
2. Функциональное программирование (каррирование)
function multiply(a) {
return function(b) {
return a * b;
};
}
const double = multiply(2);
const triple = multiply(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
3. Обработчики событий с сохранением состояния
function createButtonLogger(buttonName) {
let clickCount = 0;
return function() {
clickCount++;
console.log(`Кнопка "${buttonName}" нажата ${clickCount} раз(а)`);
};
}
const logSave = createButtonLogger('Сохранить');
const logDelete = createButtonLogger('Удалить');
// Прикрепить к кнопкам:
// saveBtn.addEventListener('click', logSave);
// deleteBtn.addEventListener('click', logDelete);
Важные моменты
- Каждый вызов внешней функции создаёт новое замыкание с собственным набором переменных.
- Замыкание хранит ссылку на переменные, а не их копии. Поэтому если изменить переменную изнутри замыкания, это изменение будет видно всем функциям, которые разделяют это окружение.
- Замыкания могут привести к утечкам памяти, если случайно сохранить ссылку на большое замыкание, которое больше не нужно. В современных браузерах это редкость, но стоит помнить.
- Классическая ошибка с замыканиями в циклах решается с помощью
let(создаёт новую переменную на каждой итерации) или через немедленно вызываемую функцию (IIFE).
Другой взгляд: замыкание как "рюкзак" с переменными
Представьте, что каждая функция — это человек, который идёт в поход. Когда он создаётся (определяется), он берёт с собой "рюкзак" — лексическое окружение, в котором он был создан. В этом рюкзаке лежат все переменные, которые были доступны в момент создания функции.
Когда внешняя функция завершается, её локальные переменные обычно исчезают. Но если внутренняя функция сохранила ссылку на этот рюкзак (через замыкание), то рюкзак остаётся жить в памяти, и внутренняя функция может пользоваться его содержимым в любой момент.
Пример с setTimeout (частая ошибка)
// Проблема: все выведут 3
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 1000);
}
// Решение через замыкание (var)
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(() => console.log(j), 1000);
})(i);
}
// Или проще — используйте let
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 1000); // 0, 1, 2
}
В первом случае все три setTimeout видят один и тот же i (который к моменту срабатывания таймеров уже стал равен 3). Во втором случае мы создаём новое замыкание на каждой итерации с помощью IIFE, и каждый таймер получает свою копию j. С let это происходит автоматически.
Краткое резюме
- Замыкание = функция + её лексическое окружение на момент создания.
- Позволяет функциям "помнить" переменные из внешней области видимости.
- Каждый вызов внешней функции создаёт новое замыкание.
- Используется для инкапсуляции, каррирования, создания фабрик функций и в асинхронном коде.
Ответов пока нет.
Войдите чтобы оставить ответ