JavaScriptMiddleTechnical
Объясните понятие closure в JavaScript. Как closure может вызвать утечку памяти?
Замыкание — функция, которая запоминает переменные из внешней области видимости. Утечка памяти возникает, когда замыкание удерживает ссылку на большой объект (DOM-узел, данные) дольше, чем нужно.
Что такое замыкание
Замыкание (closure) возникает, когда внутренняя функция обращается к переменным из внешней функции после того, как внешняя уже завершила выполнение. Движок сохраняет ссылку на лексическое окружение (scope), не давая сборщику мусора удалить переменные.
function makeCounter(start = 0) {
let count = start; // переменная живёт в замыкании
return {
increment() { return ++count; },
decrement() { return --count; },
value() { return count; },
};
}
const c = makeCounter(10);
c.increment(); // 11
c.increment(); // 12
c.value(); // 12
// count недоступен снаружи, но живёт, пока жив объект c
Практические применения
// 1. Частичное применение / каррирование
const add = (a) => (b) => a + b;
const add5 = add(5);
add5(3); // 8
// 2. Приватное состояние модуля (Module Pattern)
const store = (() => {
let state = {};
return {
get: (key) => state[key],
set: (key, val) => { state[key] = val; },
reset: () => { state = {}; },
};
})();
// 3. Мемоизация
function memoize(fn) {
const cache = new Map();
return function (...args) {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
Утечки памяти через замыкания
Утечка возникает, когда замыкание удерживает ссылку на большой объект, а сам callback/обработчик остаётся зарегистрированным после ненужности.
// Плохо: DOM-узел и data живут, пока жив обработчик
function setupLeak() {
const hugeData = new Array(100_000).fill('x');
const el = document.getElementById('btn');
el.addEventListener('click', function handler() {
console.log(hugeData.length); // замыкание держит hugeData
// handler никогда не удаляется!
});
}
// Хорошо: явно удаляем обработчик
function setupClean() {
const hugeData = new Array(100_000).fill('x');
const el = document.getElementById('btn');
function handler() {
console.log(hugeData.length);
el.removeEventListener('click', handler); // однократный обработчик
}
el.addEventListener('click', handler);
// или: el.addEventListener('click', handler, { once: true });
}
// Утечка через setInterval
function intervalLeak() {
const bigObject = { data: new Array(50_000).fill(0) };
const id = setInterval(() => {
console.log(bigObject.data.length); // bigObject никогда не GC
}, 1000);
// Нет clearInterval — bigObject держится вечно
return id; // вызывающий должен сохранить и вызвать clearInterval
}
Диагностика утечек в Chrome DevTools
- Вкладка Memory → Heap snapshot: сделать снимок до и после действия, сравнить retained size.
- Allocation timeline: найти объекты, которые растут и не освобождаются.
- Фильтр по типу: искать
Detached HTMLElement— узлы, удалённые из DOM, но удерживаемые замыканием.
Подводные камни
- Цикличные ссылки: объект ссылается на замыкание, замыкание на объект — до ES6 это гарантировало утечку в старых IE; в современных движках mark-and-sweep справляется, но в WeakMap/WeakRef-сценариях важно понимать граф ссылок.
- Замыкание в цикле with var: классический баг — все callback-и делят одну переменную
i. Решение:letили IIFE. - Большой внешний объект в маленькой функции: даже если замыканию нужна одна строка из объекта, держится весь объект — деструктурируйте нужное.
- Таймеры без clearInterval/clearTimeout: timer callback держит весь внешний scope до очистки.
- Обработчики событий на глобальных объектах:
window.addEventListener,document.addEventListener— живут весь жизненный цикл страницы. - React useEffect без cleanup: подписка без возврата функции очистки держит ссылки на state и props старого рендера.
- WebSocket / EventSource: если соединение не закрывается при unmount компонента, замыкание с callback удерживает DOM и данные.
- Скрытые замыкания в Promise chain: незавершённые promise-цепочки удерживают весь контекст создания — убедитесь, что промисы завершаются или отменяются (AbortController).
Common mistakes
- Смешивать «closure» с похожим механизмом без критерия выбора.
- Игнорировать риск: неверно оценить границы применения темы «closure» и получить хрупкое решение.
- Показывать только синтаксис и не объяснять поведение в runtime или сборке.
What the interviewer is testing
- Объясняет лексическое окружение функции и риск удержания лишних объектов в памяти.
- Показывает на примере, как работает: замыкание хранит ссылки на переменные внешней области, поэтому callback, таймер или обработчик события может продлить жизнь большого объекта после ухода компонента со страницы.
- Называет production-нюанс и граничный случай для темы «closure».