Замыкания (Closures)
Замыкание — одно из самых мощных и поначалу загадочных понятий в JavaScript. Функция «помнит» переменные из той области, где она была создана — даже после того, как эта область «закрылась». Разберём как это работает и зачем это нужно.
🔄 Вспомним: функции видят переменные родителя
В предыдущем уроке мы узнали о цепочке областей видимости: внутренняя функция видит переменные внешней функции. Это и есть основа замыканий.
function outer() {
let message = "Привет из outer!";
function inner() {
// inner видит переменную message из outer
console.log(message);
}
inner(); // "Привет из outer!"
}
outer();
Это пока ничего удивительного. Но что если внешняя функция вернёт внутреннюю? И что если мы вызовем внутреннюю позже — когда outer уже давно завершила работу?
💡 Суть замыкания: функция «захватывает» окружение
Замыкание — это функция вместе с «рюкзаком» переменных из родительской области видимости, которые она запомнила при создании. Даже после того, как внешняя функция завершила выполнение — переменные в этом «рюкзаке» продолжают жить, пока на них держится ссылка через замыкание.
function createGreeter() {
let greeting = "Привет"; // эта переменная "захватится" замыканием
function greet(name) {
// greet запомнила greeting из createGreeter
console.log(greeting + ", " + name + "!");
}
return greet; // возвращаем функцию, а не её вызов!
}
// createGreeter() выполнилась и завершилась.
// Но greet "унесла" с собой переменную greeting
let greetFn = createGreeter();
// Вызываем greet позже — greeting всё ещё доступна!
greetFn("Иван"); // "Привет, Иван!"
greetFn("Мария"); // "Привет, Мария!"
🎒 Метафора «рюкзака»: функция при создании «упаковывает» в рюкзак все переменные из своего окружения. Куда бы функцию ни передали — рюкзак всегда с ней. Это и есть замыкание.
Формальное определение: замыкание (closure) — это функция, которая имеет доступ к переменным из своей области видимости, а также из всех внешних областей видимости, в которых она была создана — даже после того, как эти внешние области завершили выполнение.
🔢 Классический пример: счётчик с приватным состоянием
Самый известный пример замыкания — функция-счётчик. Каждый вызов увеличивает счётчик. Но счётчик «спрятан» внутри — снаружи к нему не добраться и не изменить напрямую.
function createCounter() {
let count = 0; // приватная переменная — снаружи недоступна
function increment() {
count++;
console.log("Счётчик:", count);
}
return increment;
}
let counter = createCounter();
counter(); // Счётчик: 1
counter(); // Счётчик: 2
counter(); // Счётчик: 3
// Попытка получить count напрямую:
console.log(count); // ❌ ReferenceError: count is not defined
// Каждый вызов createCounter() создаёт НОВОЕ независимое замыкание:
let counter1 = createCounter();
let counter2 = createCounter();
counter1(); // Счётчик: 1
counter1(); // Счётчик: 2
counter2(); // Счётчик: 1 — свой независимый count!
🔐 Инкапсуляция данных: это мощный паттерн! Переменная count полностью защищена. Её нельзя случайно изменить из кода снаружи. Единственный способ изменить счётчик — через специально предоставленную функцию increment.
🎛️ Расширенный счётчик: объект с методами
Из замыкания можно вернуть не одну функцию, а объект с несколькими методами — все они будут иметь доступ к одним и тем же приватным переменным:
function createCounter(startValue = 0) {
let count = startValue;
return {
increment() {
count++;
},
decrement() {
count--;
},
reset() {
count = startValue;
},
getValue() {
return count;
}
};
}
let counter = createCounter(10);
counter.increment();
counter.increment();
counter.increment();
console.log(counter.getValue()); // 13
counter.decrement();
console.log(counter.getValue()); // 12
counter.reset();
console.log(counter.getValue()); // 10 (вернулись к startValue)
// count по-прежнему недоступен напрямую:
console.log(counter.count); // undefined — нет такого свойства!
Это паттерн модуля — один из самых важных паттернов в JavaScript. Данные спрятаны, доступ только через публичный интерфейс.
🏭 Фабрика функций: makeMultiplier
Ещё один популярный паттерн с замыканиями — фабрика функций: функция, которая создаёт и возвращает другие функции, «настроенные» под конкретный параметр.
// Фабрика умножителей
function makeMultiplier(factor) {
// factor "захватывается" замыканием
return function(number) {
return number * factor;
};
}
// Создаём конкретные умножители:
let double = makeMultiplier(2);
let triple = makeMultiplier(3);
let tenTimes = makeMultiplier(10);
console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log(tenTimes(5)); // 50
// Можно применять к массивам:
let numbers = [1, 2, 3, 4, 5];
console.log(numbers.map(double)); // [2, 4, 6, 8, 10]
console.log(numbers.map(triple)); // [3, 6, 9, 12, 15]
// Ещё пример: фабрика приветствий
function makeGreeter(language) {
const greetings = {
ru: "Привет",
en: "Hello",
de: "Hallo",
es: "Hola"
};
let greeting = greetings[language] || "Hi";
return function(name) {
return greeting + ", " + name + "!";
};
}
let greetRu = makeGreeter("ru");
let greetEn = makeGreeter("en");
console.log(greetRu("Иван")); // "Привет, Иван!"
console.log(greetEn("John")); // "Hello, John!"
🪤 Частая ошибка: замыкания в цикле с var
Это одна из самых известных ловушек JavaScript — часто встречается на собеседованиях. Если создавать замыкания в цикле с var, все они будут использовать одну и ту же переменную:
// ❌ ОШИБКА: var — одна переменная i для всех замыканий
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // все выведут 3, а не 0, 1, 2!
}, 1000);
}
// Через 1 секунду: 3, 3, 3 — не то, что ожидали!
// ✅ РЕШЕНИЕ: let создаёт новую переменную для каждой итерации
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 0, 1, 2 — правильно!
}, 1000);
}
// Через 1 секунду: 0, 1, 2
С let на каждой итерации создаётся новая переменная i — каждый колбэк замыкает свою копию. С var все колбэки замыкают одну и ту же переменную i, которая к моменту вызова уже равна 3.
🌐 Где замыкания используются в реальном коде
Колбэк «помнит» переменные из момента создания — это замыкание.
Функция-обработчик «помнит» данные, которые были в момент её создания (например, id элемента).
Паттерн модуля — приватные данные через замыкание, публичный API через возвращаемый объект.
Кэширование результатов вычислений — хранится в переменной внутри замыкания.
// Мемоизация через замыкание:
function memoize(fn) {
let cache = {}; // кэш "живёт" в замыкании
return function(n) {
if (n in cache) {
console.log("Из кэша:", n);
return cache[n];
}
let result = fn(n);
cache[n] = result;
return result;
};
}
function slowSquare(n) {
// Имитируем медленное вычисление
return n * n;
}
let fastSquare = memoize(slowSquare);
fastSquare(5); // Вычисляет: 25
fastSquare(5); // Из кэша: 5 → 25 (не вычисляет снова)
fastSquare(7); // Вычисляет: 49
✅ Итоги урока
- Замыкание — функция + переменные из внешней области видимости, «захваченные» при создании
- Внешняя функция завершилась — переменные живут, пока существует ссылка на внутреннюю функцию
- Паттерн инкапсуляции: приватные данные в замыкании, доступ только через публичные функции
- Каждый вызов внешней функции создаёт новое независимое замыкание со своими переменными
- Фабрика функций — создаёт «настроенные» функции через замыкание параметра
- В цикле замыкания:
var— общая переменная для всех,let— новая на каждой итерации - Реальное применение: setTimeout, обработчики событий, модули, мемоизация
В следующем уроке — рекурсия: функции, которые вызывают сами себя. Это ещё один мощный инструмент, особенно для работы с вложенными структурами. 🚀
Тест: Замыкания (Closures)
6 вопросов