Замыкание в JavaScript
Замыкание в JavaScript
Почему тема замыканий сложная
Замыкание сложно, потому что нужно одновременно держать в голове:
- где функция объявлена;
- где она вызвана;
- какие переменные она "видит" по лексической области видимости;
- сколько живет это состояние после завершения внешней функции.
Ключевая мысль: замыкание - это не отдельный синтаксис, а поведение обычных функций в JavaScript.
Шаг 1: лексическая область видимости
Функция ищет переменные не по месту вызова, а по месту объявления.
const level = 'global';
function outer() {
const level = 'outer';
function inner() {
console.log(level);
}
inner();
}
outer(); // outer
inner берет level из outer, потому что объявлена внутри outer.
Шаг 2: что такое замыкание
Замыкание возникает, когда внутренняя функция продолжает использовать внешние переменные даже после завершения внешней функции.
function createGreeter(prefix) {
return function (name) {
return `${prefix}, ${name}`;
};
}
const hi = createGreeter('Привет');
console.log(hi('Анна')); // Привет, Анна
prefix продолжает жить внутри возвращенной функции hi.
Смотри, что важно: замыкание "захватывает" переменную, а не копирует ее значение один раз. Если переменная меняется, внутренняя функция будет видеть новое значение:
let x = 1;
function makeLogger() {
return function () {
console.log(x);
};
}
const logX = makeLogger();
x = 2;
logX(); // 2
Классический пример: счетчик
function createCounter() {
let count = 0;
return function () {
count += 1;
return count;
};
}
const next = createCounter();
console.log(next()); // 1
console.log(next()); // 2
Переменная count приватная: снаружи к ней нет прямого доступа, но замыкание ее сохраняет между вызовами.
Приватное состояние через замыкание
function createUserState() {
let isOnline = false;
return {
setOnline() {
isOnline = true;
},
setOffline() {
isOnline = false;
},
getStatus() {
return isOnline;
},
};
}
Это базовый паттерн инкапсуляции: состояние есть, но управляется только через API.
Фабрики функций (параметризация логики)
function createMinValidator(min) {
return function (value) {
return value >= min;
};
}
const isAdult = createMinValidator(18);
console.log(isAdult(17)); // false
console.log(isAdult(20)); // true
Каждая созданная функция хранит свой min.
Важно: разделяемое и изолированное состояние
Если использовать одно и то же замыкание - состояние общее:
const counter = createCounter();
const a = counter;
const b = counter;
console.log(a()); // 1
console.log(b()); // 2
Если вызвать фабрику два раза - состояние изолировано:
const c1 = createCounter();
const c2 = createCounter();
console.log(c1()); // 1
console.log(c1()); // 2
console.log(c2()); // 1
Это одна из самых важных идей для UI-компонентов и сервисов.
Частая ловушка: var в цикле
const fns = [];
for (var i = 0; i < 3; i++) {
fns.push(function () {
console.log(i);
});
}
fns[0](); // 3
fns[1](); // 3
fns[2](); // 3
С var одна общая переменная i на весь цикл, и к моменту вызова она уже равна 3.
Правильный вариант для новичка - let:
const fns2 = [];
for (let j = 0; j < 3; j++) {
fns2.push(function () {
console.log(j);
});
}
fns2[0](); // 0
fns2[1](); // 1
fns2[2](); // 2
С let на каждой итерации создается новая переменная j.
Замыкание в асинхронном коде
function createDelayedLogger(label) {
return function () {
setTimeout(() => {
console.log(label);
}, 100);
};
}
const logSave = createDelayedLogger('saved');
logSave(); // через ~100мс: saved
label сохраняется в замыкании до момента выполнения setTimeout.
Где применять замыкания осознанно
- приватное состояние без глобальных переменных;
- фабрики функций (
createValidator,createFormatter); - настройка обработчиков с контекстом;
- утилиты вроде
debounce/throttle.
Проверь себя: почему замыкание удобнее глобальной переменной для счетчика конкретного компонента?
Частые ошибки новичков
- думать, что замыкание копирует значение один раз и "замораживает" его;
- не понимать, делится ли состояние между ссылками;
- использовать
varв циклах с отложенными вызовами; - хранить в замыкании слишком большие данные без необходимости.
Анти-провал: всегда отвечай на 2 вопроса: "кто владеет состоянием?" и "когда это состояние должно умереть?".
Краткий итог
- Замыкание - это функция + доступ к внешним переменным из места объявления.
- Внешние переменные могут жить дольше внешней функции, если их использует внутренняя.
- Замыкания дают приватность и переиспользуемые фабрики поведения.
- Один вызов фабрики = одно состояние, если не делишь одну и ту же ссылку.
- Понимание
var/letи жизненного цикла переменных критично для корректной работы замыканий.