Замыкания (Closures)
Замыкание — функция, которая запоминает и имеет доступ к переменным из своего лексического окружения, даже когда выполняется вне этого окружения.
Зачем нужно
Замыкания — один из фундаментальных механизмов JavaScript. Они позволяют создавать приватные переменные, фабрики функций, мемоизацию, модульный паттерн. Понимание замыканий — ключ к пониманию того, как работает JavaScript «под капотом».
Где используется
Приватные переменные, счётчики, каррирование, мемоизация, модульный паттерн, обработчики событий, callback-функции, debounce/throttle.
Предпосылки
Function Declaration, Function Expression, Переменные
Лексическое окружение
Каждая функция при создании получает ссылку на лексическое окружение (Lexical Environment), в котором она была определена:
function outer() {
const message = 'Привет'; // переменная внешней функции
function inner() {
console.log(message); // доступ к переменной outer
}
return inner;
}
const fn = outer; // outer завершилась
fn; // "Привет" — message всё ещё доступна!
Что происходит
outerсоздаёт переменнуюmessageи функциюinnerinner«замыкает» переменнуюmessage(сохраняет ссылку на окружение)outerвозвращаетinnerи завершается- Но
messageНЕ уничтожается — на неё есть ссылка изinner - Вызов
fnчитаетmessageиз замкнутого окружения
Практические примеры
Счётчик
function createCounter(initial = 0) {
let count = initial;
return {
increment { return ++count; },
decrement { return --count; },
getCount { return count; },
reset { count = initial; }
};
}
const counter = createCounter(10);
console.log(counter.increment); // 11
console.log(counter.increment); // 12
console.log(counter.decrement); // 11
console.log(counter.getCount); // 11
// count недоступен напрямую — это приватная переменная!
// console.log(counter.count); // undefined
Приватные переменные
function createBankAccount(initialBalance) {
let balance = initialBalance;
const history = ;
function log(action, amount) {
history.push({
action,
amount,
balance,
date: new Date.toISOString()
});
}
return {
deposit(amount) {
if (amount <= 0) throw new Error('Сумма должна быть положительной');
balance += amount;
log('deposit', amount);
return balance;
},
withdraw(amount) {
if (amount > balance) throw new Error('Недостаточно средств');
balance -= amount;
log('withdraw', amount);
return balance;
},
getBalance { return balance; },
getHistory { return [...history]; } // копия, не оригинал
};
}
const account = createBankAccount(1000);
account.deposit(500); // 1500
account.withdraw(200); // 1300
console.log(account.getBalance); // 1300
// balance и history недоступны извне
Фабрика функций
function createGreeter(greeting) {
return function(name) {
return `${greeting}, ${name}!`;
};
}
const helloGreeter = createGreeter('Привет');
const goodbyeGreeter = createGreeter('До свидания');
console.log(helloGreeter('Иван')); // "Привет, Иван!"
console.log(goodbyeGreeter('Мария')); // "До свидания, Мария!"
Мемоизация
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log('Из кэша');
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
const expensiveCalc = memoize((n) => {
console.log('Вычисляю...');
return n * n;
});
expensiveCalc(5); // "Вычисляю..." → 25
expensiveCalc(5); // "Из кэша" → 25
Модульный паттерн
const Logger = (function {
let logs = ;
let level = 'info';
function formatMessage(msg, lvl) {
return `[${lvl.toUpperCase()}] ${new Date.toISOString()}: ${msg}`;
}
return {
setLevel(newLevel) { level = newLevel; },
log(msg) {
const formatted = formatMessage(msg, level);
logs.push(formatted);
console.log(formatted);
},
getLogs { return [...logs]; },
clear { logs = ; }
};
});
Logger.log('Приложение запущено');
Logger.setLevel('error');
Logger.log('Что-то пошло не так');
Замыкания и циклы
Классическая ловушка с var
// Проблема: var имеет функциональную область видимости
for (var i = 0; i < 3; i++) {
setTimeout( => console.log(i), 100);
}
// Вывод: 3, 3, 3 — все функции замкнулись на одну переменную i
// Решение 1: let (блочная область)
for (let i = 0; i < 3; i++) {
setTimeout( => console.log(i), 100);
}
// Вывод: 0, 1, 2
// Решение 2: IIFE (до ES6)
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout( => console.log(j), 100);
})(i);
}
// Вывод: 0, 1, 2
Замыкание и память
Замыкания удерживают ссылку на всё лексическое окружение:
function createHeavyClosure() {
const largeData = new Array(1000000).fill('data');
// Эта функция замыкает largeData, даже если не использует его
return function {
return 'Замыкание создано';
};
}
// largeData останется в памяти, пока жива функция
const fn = createHeavyClosure;
// Для освобождения: fn = null;
Минимизация утечек
function createOptimized() {
const largeData = new Array(1000000).fill('data');
const summary = largeData.length; // вычислить нужное заранее
// Замыкаем только summary, не largeData
return function {
return `Элементов: ${summary}`;
};
// largeData будет собран GC, т.к. нет ссылок
}
Замыкание и this
function Timer() {
this.seconds = 0;
// Стрелочная функция замыкает this из Timer
setInterval(() => {
this.seconds++;
}, 1000);
}
// Альтернатива: сохранить this в переменную (до ES6)
function TimerOld() {
const self = this; // замыкаем this
this.seconds = 0;
setInterval(function {
self.seconds++; // используем замкнутый self
}, 1000);
}
Частые ошибки
1. Замыкание в цикле с var
// См. раздел "Замыкания и циклы" выше
// Всегда используйте let в циклах с асинхронными операциями
2. Утечки памяти через замыкания
function setupHandler() {
const element = document.getElementById('button');
const heavyData = loadHugeDataset;
element.addEventListener('click', () => {
// Замыкает heavyData, даже если не нужна
console.log('Клик!');
});
// Решение: обнулить ненужные ссылки
// heavyData = null; // если данные больше не нужны
}
3. Ожидание копии вместо ссылки
function example() {
let value = 1;
const getValue = () => value;
const setValue = (v) => { value = v; };
setValue(42);
console.log(getValue); // 42, а не 1!
// Замыкание хранит ССЫЛКУ, не копию
}
Практика
- Создай счётчик с методами
increment,decrement,reset - Напиши фабрику функций
createMultiplier(factor)→fn(n) => n * factor - Реализуй приватную переменную через замыкание
- Напиши функцию
once(fn)— вызоветfnтолько один раз - Исправь проблему с
varв цикле тремя способами (let, IIFE, bind)
Связанные темы
Ресурсы
🎓 Источник: Функции, стрелочные функции, контексты, замыкания
- 📅 2018-09-27 · YouTube · ID:
pn5myCmpV2U - Тезисы:
- Замыкание = контекст, который может хранить состояние
- Контексты вложены как матрёшка: блок, for, if тоже создают контекст
- В чистом ФП состояние замыкания не меняется
- К значениям замыкания нет прямого доступа —
console.logпечатаетfunction - Простейшее замыкание
hashхранит data и counter; экспортируем внутреннюю лямбду - Рекурсивное замыкание-сумматор:
(x) => (y) => add(x + y)для бесконечной цепочки
- Цитата: «Контексты, в которых лежат функции и состояние, имеют свойство выходить из-под контроля программиста, и нужно как можно лучше за этим всем следить.»
- Код:
const hash = () => { const data = {}; let counter = 0; return (key, value) => { data[key] = value; counter++; return data; }; };
⚡ Источник: Замыкания с точки зрения официальной спецификации · AsForJS
- 📅 2025-07-05 · YouTube · ID:
RvYq-wt_GEU - Тезисы:
- В спеке ECMAScript термина «closure» нет — это академический термин CS
- Замыкание = процесс связывания окружений, не «функция из функции»
- JS решает восходящую funarg-проблему (lexical/static scoping)
varлежит в Variable Environment,let/const— в Lexical Environment (разные!)- Замыкание возникает в момент определения функции, не возврата
- Даже блок
{ let a }создаёт замыкание по спеке - V8 не сохраняет окружение, если оно не используется — нарушает спеку ради скорости
- Стрелка
=> x— единственная настоящая константа в JS
- Цитата: «Замыканием называется процесс, когда одно окружение связывается с другим окружением, что позволяет коду находить идентификаторы в своем или в окружении выше.»
- Подробнее: Замыкания -- funarg-проблема