Что такое замыкания в JavaScript: объяснение на примерах
Основы3 минРедакция Innovialabs
Коротко
Замыкание — это функция, которая помнит переменные из места, где была создана, и продолжает иметь к ним доступ даже после того, как внешняя функция завершилась.
Что такое замыкание
Замыкание — это функция вместе с переменными из окружения, в котором она была создана. Функция «помнит» эти переменные и может читать и менять их, даже когда внешняя функция уже отработала.
function makeGreeting(name) {
return function () {
return `Привет, ${name}!`;
};
}
const hiAnna = makeGreeting('Анна');
hiAnna(); // "Привет, Анна!"
makeGreeting давно завершилась, но внутренняя функция всё ещё видит name. Это и есть замыкание.
Лексическая область видимости
Чтобы понять замыкания, нужно одно правило: функция видит переменные там, где она написана в коде, а не там, где её вызвали. Это называется лексической областью видимости.
const color = 'синий';
function show() {
console.log(color);
}
function run() {
const color = 'красный';
show();
}
run(); // "синий"
show объявлена на верхнем уровне, поэтому видит внешний color, а не тот, что внутри run.
Поиск переменной идёт по цепочке: сначала текущая функция, затем внешняя, и так до глобальной области.
Практическое применение
Счётчик
function createCounter() {
let count = 0;
return {
increment: () => ++count,
get: () => count,
};
}
const counter = createCounter();
counter.increment();
counter.increment();
counter.get(); // 2
Каждый вызов createCounter создаёт свою переменную count. Два счётчика не мешают друг другу.
Приватное состояние
Снаружи до count не добраться — только через методы. Это способ скрыть данные без классов. Похожим образом работают модули, хуки в React (useState опирается на замыкания) и обработчики событий.
Мемоизация
Замыкание может хранить кэш результатов:
function memoize(fn) {
const cache = new Map();
return (n) => {
if (cache.has(n)) return cache.get(n);
const result = fn(n);
cache.set(n, result);
return result;
};
}
const slowSquare = (n) => n * n;
const fastSquare = memoize(slowSquare);
cache живёт внутри замыкания и сохраняется между вызовами.
Классический баг: var в цикле
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// 3, 3, 3
Почему не 0, 1, 2? У var функциональная область видимости: на весь цикл одна переменная i. К моменту запуска таймеров цикл уже закончился, и i равна 3. Все три функции замкнули одну и ту же переменную.
Решение — let. У него блочная область видимости, и на каждую итерацию создаётся новая i:
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// 0, 1, 2
Частые ошибки
- Ожидать копию значения. Замыкание хранит ссылку на переменную, а не её значение в момент создания. Если переменная изменится, функция увидит новое значение.
- Устаревшие значения в React. Обработчик, созданный на одном рендере, видит state этого рендера. Отсюда «stale closure» в
useEffectиsetInterval. - Утечки памяти. Пока замыкание живо, живут и переменные, на которые оно ссылается. Снимайте ненужные обработчики событий и таймеры.