Что такое замыкание (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 остаётся в памяти, потому что на неё есть ссылка из замыкания.