Что такое замыкание (closure) и где оно может пригодиться?

Замыкание (closure) — это функция, которая "замыкает" в себе (сохраняет доступ) переменные из внешней области видимости, даже после того как эта внешняя функция завершила выполнение.

1. Механизм замыкания

Когда функция создаётся в JavaScript, она "запоминает" окружение (лексическую область видимости), в котором была определена. Это окружение сохраняется в памяти, и функция может продолжать использовать переменные, даже если внешняя функция уже завершилась.

function outer() {
let counter = 0;
return function inner() {
counter++;
return counter;
}
}
const increment = outer();
console.log(increment()); // 1
console.log(increment()); // 2
console.log(increment()); // 3

В этом примере inner — замыкание. Оно имеет доступ к переменной counter, даже после завершения outer.

2. Почему это возможно

В JavaScript функции — это объекты первого класса. Когда функция создается, она не только содержит свой код, но и замыкает лексическое окружение — переменные, которые доступны в момент её создания. Это окружение хранится в памяти, пока замыкание живо.

3. Где используются замыкания

3.1. Инкапсуляция данных

Позволяет создать "приватные" переменные и методы, недоступные снаружи.

function createUser(name) {
let password = 'secret123';
return {
getName() {
return name;
},
checkPassword(p) {
return p === password;
}
};
}
const user = createUser('Alice');
console.log(user.getName()); // Alice
console.log(user.checkPassword('secret123')); // true
console.log(user.password); // undefined

3.2. Генераторы функций (фабрики)

Позволяет создавать функции с предустановленным поведением.

function multiplier(factor) {
return function (number) {
return number \* factor;
};
}
const double = multiplier(2);
const triple = multiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15

3.3. Подсчёт состояния между вызовами

function createCounter() {
let count = 0;
return function () {
count++;
return count;
}
}
const counter1 = createCounter();
const counter2 = createCounter();
console.log(counter1()); // 1
console.log(counter1()); // 2
console.log(counter2()); // 1

3.4. Таймеры и асинхронные операции

function delayLog(message, delay) {
setTimeout(function () {
console.log(message);
}, delay);
}
delayLog('Привет', 1000);

Функция внутри setTimeout замыкает переменную message.

4. Проблемы с замыканиями в цикле (до ES6)

В старом коде (с var) часто происходили ошибки при использовании замыканий в цикле:

for (var i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i);
}, 100);
}
// 3, 3, 3

Причина: одна и та же переменная i замыкается каждой функцией, и к моменту вызова её значение становится 3.

Решение — использовать IIFE или let:

С IIFE:

for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(function () {
console.log(j);
}, 100);
})(i);
}
// 0, 1, 2

С let:

for (let i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i);
}, 100);
}
// 0, 1, 2

5. Реализация once-функции

Функция, которая выполняется только один раз:

function once(fn) {
let called = false;
let result;
return function (...args) {
if (!called) {
result = fn.apply(this, args);
called = true;
}
return result;
}
}
const init = once(() => {
console.log('Инициализация...');
return 42;
});
init(); // Инициализация...  42
init(); //  42

Переменные called и result замкнуты внутри возвращаемой функции.

6. Создание приватных счётчиков или состояний в функциях

const counter = (function () {
let count = 0;
return {
increment() {
count++;
return count;
},
decrement() {
count--;
return count;
},
get() {
return count;
}
};
})();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.get()); // 2

Здесь count недоступен извне — только через методы.

7. Использование в обработчиках событий

function bindClickHandler(id) {
let count = 0;
document.getElementById(id).addEventListener('click', function () {
count++;
console.log(\`Нажатий: ${count}\`);
});
}

Функция-обработчик сохраняет доступ к count, хотя bindClickHandler уже завершила выполнение.

8. Использование в debounce / throttle

function debounce(fn, delay) {
let timeout;
return function (...args) {
clearTimeout(timeout);
timeout = setTimeout(() => fn.apply(this, args), delay);
};
}

timeout живёт внутри замыкания и сохраняется между вызовами.

9. Паттерн модуль (module pattern)

const Module = (function () {
let privateData = 'секрет';
return {
get() {
return privateData;
},
set(value) {
privateData = value;
}
};
})();
console.log(Module.get()); // 'секрет'
Module.set('новый секрет');
console.log(Module.get()); // 'новый секрет'

privateData замкнута и недоступна напрямую.

10. Частичное применение (partial application)

function partial(fn, ...fixedArgs) {
return function (...restArgs) {
return fn(...fixedArgs, ...restArgs);
};
}
function sum(a, b, c) {
return a + b + c;
}
const add5 = partial(sum, 2, 3);
console.log(add5(4)); // 9

fixedArgs хранятся в замыкании.

11. Пример с лексическим окружением

function outer() {
let a = 1;
function inner() {
console.log(a); // a захвачен через замыкание
}
return inner;
}
const f = outer();
f(); // 1

Переменная a остаётся в памяти, потому что на неё есть ссылка из замыкания.