Замыкания в JavaScript: простое объяснение, примеры и разбор работы изнутри

Замыкания в JavaScript — это механизм, благодаря которому функция «запоминает» переменные из того места, где была создана, даже если выполняется позже и в совершенно другом контексте. Если сказать проще: замыкание — это когда функция «помнит» переменные, которые были вокруг неё в момент создания, даже если та внешняя функция уже закончила работу.
На практике это работает следующим образом: каждая функция в момент создания образует связь с окружением, в котором была объявлена. Эта связь — и есть замыкание. Оно позволяет внутренней функции обращаться к переменным родительской, хотя формально родительская уже завершила работу и её контекст выполнения удалён из стека вызовов.
Замыкания применяются повсеместно в современной разработке. Мы используем их для создания приватных переменных (имитируя инкапсуляцию), в обработчиках событий (чтобы функция «помнила» нужное значение на момент навешивания обработчика), в функциях-фабриках, модулях, каррировании и других продвинутых паттернах. По сути, этот механизм — тот самый «клей», который связывает данные и поведение в JavaScript, делая язык гибким и выразительным инструментом.

Функция в JavaScript похожа на туриста: она путешествует по коду, всегда имея при себе «рюкзак» (замыкание) с необходимыми ей данными и переменными из места своего создания.
Вот простейший пример замыкания:
function outerFunction() {
let message = 'Привет из внешней функции';
return function innerFunction() {
console.log(message);
};
}
const myFunction = outerFunction();
myFunction(); // 'Привет из внешней функции'
Здесь innerFunction «захватывает» переменную message из outerFunction, и даже после завершения работы outerFunction эта переменная остаётся доступной через замыкание.
- Зачем нужны замыкания: ключевые задачи и примеры применения
- Как работают замыкания: логика шаг за шагом
- Примеры замыканий от простого к сложному
- Замыкания в циклах: почему возникают ошибки и как их исправлять
- Частые ошибки при работе с замыканиями и как их избежать
- Продвинутые паттерны и полезные техники на замыканиях
- Замыкания и производительность: когда они вредят коду
- Заключение
- Рекомендуем посмотреть курсы по JavaScript разработке
Зачем нужны замыкания: ключевые задачи и примеры применения
Замыкания решают целый спектр практических задач, которые возникают в процессе разработки. Рассмотрим основные сценарии их применения:
- Приватные переменные. Они позволяют создавать переменные, недоступные извне, но сохраняющие своё состояние между вызовами. Это классический способ имитации инкапсуляции в JavaScript до появления приватных полей через символ #.
- Приватные методы. По аналогии с переменными, мы можем скрывать вспомогательные функции, делая их доступными только внутри — так публичный API остаётся чистым и понятным.
- Инкапсуляция состояния. Данный механизм даёт возможность хранить данные, которые изменяются только через контролируемые интерфейсы. Внешний код не может напрямую модифицировать внутреннее состояние — только через предоставленные методы.
- Использование в обработчиках событий. Когда навешивается обработчик события, часто требуется «заморозить» определённое значение на момент создания обработчика. Замыкание сохраняет это значение, даже если DOM изменится или переменная получит новое.
- Фабрики функций и модули. Замыкания лежат в основе паттерна «модуль», позволяя создавать независимые экземпляры с собственным состоянием. Каждый вызов функции-фабрики порождает новое с уникальным набором данных.
Вот практический пример счётчика — одна из классических демонстраций:
function createCounter() {
let count = 0;
return {
increment: function() {
count++;
return count;
},
decrement: function() {
count--;
return count;
},
getValue: function() {
return count;
}
};
}
const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getValue()); // 2
console.log(counter.decrement()); // 1
Переменная count полностью приватна — к ней нет прямого доступа снаружи. Изменять значение можно исключительно через методы increment и decrement, что обеспечивает контролируемое управление состоянием.
Ещё один пример — фабрика для создания приветствий:
function createGreeter(greeting) {
return function(name) {
console.log(`${greeting}, ${name}!`);
};
}
const sayHello = createGreeter('Привет');
const sayHi = createGreeter('Здравствуй');
sayHello('Анна'); // 'Привет, Анна!'
sayHi('Пётр'); // 'Здравствуй, Пётр!'
Каждая созданная функция запоминает свой собственный greeting благодаря замыканию, что позволяет создавать специализированные версии с предустановленными параметрами.
Как работают замыкания: логика шаг за шагом
Чтобы по-настоящему понять этот механизм, необходимо разобраться в процессах, которые работают «под капотом» JavaScript-движка. Давайте последовательно рассмотрим ключевые концепции, формирующие основу замыканий.
Лексическое окружение (Lexical Environment)
Лексическое окружение — это внутренняя структура данных, которую JavaScript-движок создаёт при каждом запуске функции или блока кода. Можно представить его как невидимый объект, хранящий всю информацию о доступных переменных и связях с внешним контекстом.
Каждое лексическое окружение состоит из двух компонентов:
- Environment Record — записи окружения, где хранятся все локальные переменные, параметры и объявления в виде пар «имя-значение». Это своего рода «словарь» всех идентификаторов, доступных в данной области.
- Ссылка на внешнее окружение (outer) — указатель на родительское лексическое окружение. Эта ссылка формирует цепочку областей видимости и позволяет обращаться к переменным из внешних контекстов. Для глобального окружения значение outer равно null, так как выше него ничего нет.
Принцип работы прост: когда функция пытается обратиться к переменной, движок сначала ищет её в текущем Environment Record. Если переменная не найдена, поиск продолжается по ссылке outer во внешнем окружении — и так далее по цепочке, пока переменная не будет найдена или не достигнут глобальный уровень. Именно эта механика делает возможными замыкания: внутренняя функция сохраняет ссылку на окружение, в котором была создана, и может обращаться к его переменным даже после завершения родительской.
Области видимости: глобальная, функциональная и блочная
Область видимости (scope) определяет, где именно в коде доступна та или иная переменная. В JavaScript существуют три основных типа областей видимости, и понимание различий между ними критически важно для работы с замыканиями.
- Глобальная область видимости охватывает весь код программы. Переменные, объявленные вне функций и блоков, попадают в глобальное окружение и доступны отовсюду. Однако злоупотребление глобальными переменными считается плохой практикой — они загрязняют пространство имён и создают риск конфликтов.
- Функциональная область видимости создаётся при каждом вызове. Переменные, объявленные внутри с помощью var, let или const, существуют только в рамках этой функции и недоступны снаружи.
- Блочная область видимости появилась с введением let и const в ES6. Переменные, объявленные этими ключевыми словами внутри блока (например, в if, for или просто в фигурных скобках {}), существуют только в пределах этого блока.
Ключевое различие между var и let/const: переменная, объявленная через var, имеет функциональную область видимости, тогда как let и const создают блочную. На практике это означает, что var в цикле for создаёт одну переменную на весь цикл, а let — новую на каждой итерации. Эта особенность критична при работе с замыканиями в циклах, о чём мы подробнее поговорим далее.
Цепочка областей видимости (Scope Chain)
Когда функция обращается к переменной, JavaScript использует механизм, называемый цепочкой областей видимости (scope chain). Это упорядоченная последовательность лексических окружений, по которой движок ищет нужный идентификатор.
Поиск начинается с текущего окружения — если переменная найдена, поиск завершается. Если нет, движок переходит к внешнему по ссылке outer, затем к следующему внешнему — и так до глобального уровня. Если переменная не обнаружена и на глобальном уровне, возникает ошибка ReferenceError.
Рассмотрим пример:
const global = 'глобальная';
function outer() {
const outerVar = 'внешняя';
function inner() {
const innerVar = 'внутренняя';
console.log(innerVar); // находится локально
console.log(outerVar); // поиск идёт во внешнее окружение
console.log(global); // поиск доходит до глобального уровня
}
inner();
}
outer();
Цепочка областей видимости для inner выглядит так: inner → outer → global. При каждом обращении к переменной движок последовательно проверяет эти уровни.
Важный момент: цепочка областей видимости формируется статически, в момент определения функции, а не динамически при её вызове. Это и называется лексической (статической) областью видимости — функция «помнит», где была объявлена, независимо от того, откуда её вызывают.

Когда функция ищет переменную, она сначала проверяет свою локальную область, затем внешнюю, и так далее вверх по цепочке до глобальной области видимости.
Контекст выполнения и стек вызовов
Контекст выполнения (Execution Context) — это абстрактная концепция из спецификации ECMAScript, описывающая среду, в которой выполняется JavaScript-код. Когда интерпретатор запускает программу, он создаёт глобальный контекст выполнения. Когда вызывается функция, создаётся новый контекст для неё.
Каждый контекст выполнения включает в себя:
- Лексическое окружение (о котором мы говорили выше).
- Значение this.
- Ссылку на внешнее окружение.
Все контексты организованы в структуру данных, называемую стеком вызовов (call stack). Стек работает по принципу LIFO (Last In, First Out) — последним пришёл, первым ушёл. Когда функция вызывается, её контекст помещается на вершину стека. Когда она завершает работу, её контекст удаляется, и управление возвращается предыдущему.
Вот как это работает:
function first() {
console.log('Начало first');
second();
console.log('Конец first');
}
function second() {
console.log('Внутри second');
}
first();
Последовательность работы стека:
- Глобальный контекст (всегда в основании стека).
- Вызов first() — добавляется контекст first на стек.
- Внутри first вызывается second() — добавляется контекст second.
- second() завершается — контекст second удаляется.
- Выполнение возвращается в first().
- first() завершается — контекст first удаляется.
- Остаётся только глобальный контекст.
Критически важно понимать: хотя контекст выполнения удаляется из стека после завершения, лексическое окружение может оставаться в памяти, если на него существует ссылка через замыкание. Именно поэтому внутренняя функция может обращаться к переменным внешней даже после того, как та завершила работу — её лексическое окружение продолжает жить благодаря этому механизму.
Примеры замыканий от простого к сложному
Теория важна, но именно практические примеры помогают по-настоящему прочувствовать, как работают замыкания. Давайте разберём несколько типичных сценариев — от базовых до более продвинутых.
Пример №1: базовая функция, запоминающая переменную
Начнём с самого простого случая — функции, которая захватывает переменную из внешней области:
function createMessage() {
const message = 'Это сообщение из замыкания';
return function showMessage() {
console.log(message);
};
}
const displayMessage = createMessage();
displayMessage(); // 'Это сообщение из замыкания'
Здесь showMessage формирует замыкание над переменной message. Несмотря на то что createMessage уже завершила работу, переменная message остаётся доступной благодаря этому механизму. Это базовый принцип, лежащий в основе всех остальных паттернов.
Пример №2: счётчик (counter) как хранение состояния
Счётчик — классический пример, демонстрирующий способность сохранять состояние между вызовами:
function createCounter() {
let count = 0;
return function() {
count++;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
Переменная count существует в единственном экземпляре для данного замыкания. При каждом вызове counter() она инкрементируется, сохраняя своё значение между вызовами. Важный момент: если создать второй счётчик через const counter2 = createCounter(), он будет иметь собственную независимую переменную count.
Пример №3: фабрика функций (function factory)
Фабрики позволяют создавать специализированные функции с предустановленными параметрами:
function createMultiplier(multiplier) {
return function(number) {
return number * multiplier;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
Каждая созданная запоминает свой собственный параметр multiplier через замыкание. Это позволяет создавать целые семейства родственных функций с минимальным количеством кода. На практике подобный подход часто используется для конфигурирования обработчиков, валидаторов или форматтеров.

Фабрика функций — это как конвейер, который штампует новые функции по одному шаблону, но с разными настройками (например, «Удвоитель» или «Утроитель»). Каждая созданная функция запоминает свои параметры благодаря замыканию.
Пример №4: приватные методы
Замыкания позволяют создавать настоящую инкапсуляцию с приватными методами, недоступными извне:
function createBankAccount(initialBalance) {
let balance = initialBalance;
function validateAmount(amount) {
return amount > 0 && typeof amount === 'number';
}
return {
deposit: function(amount) {
if (validateAmount(amount)) {
balance += amount;
return `Зачислено: ${amount}. Баланс: ${balance}`;
}
return 'Некорректная сумма';
},
withdraw: function(amount) {
if (validateAmount(amount) && amount <= balance) {
balance -= amount;
return `Снято: ${amount}. Баланс: ${balance}`;
}
return 'Недостаточно средств или некорректная сумма';
},
getBalance: function() {
return balance;
}
};
}
const account = createBankAccount(1000);
console.log(account.deposit(500)); // 'Зачислено: 500. Баланс: 1500'
console.log(account.withdraw(200)); // 'Снято: 200. Баланс: 1300'
console.log(account.getBalance()); // 1300
Вспомогательная функция validateAmount и переменная balance полностью скрыты от внешнего кода. Манипулировать балансом можно исключительно через публичные методы, что обеспечивает контроль и безопасность данных.
Пример №5: замыкания в обработчиках событий
Они особенно полезны при работе с асинхронными операциями и обработчиками событий:
function setupButtons() {
const buttons = document.querySelectorAll('.action-button');
buttons.forEach((button, index) => {
const buttonId = `button-${index}`;
button.addEventListener('click', function() {
console.log(`Нажата кнопка ${buttonId}`);
console.log(`Текст кнопки: ${button.textContent}`);
});
});
}
setupButtons();
Каждый обработчик формирует замыкание над переменными buttonId и button, специфичными для конкретной итерации цикла. Благодаря этому при клике на кнопку мы получаем именно те данные, которые относятся к ней, а не последние значения из цикла.
Ещё один практический пример — задержка с сохранением контекста:
function delayedGreetings(names) {
names.forEach((name, index) => {
setTimeout(() => {
console.log(`Привет, ${name}!`);
}, index * 1000);
});
}
delayedGreetings(['Анна', 'Борис', 'Вера']);
// Через 0 секунд: 'Привет, Анна!'
// Через 1 секунду: 'Привет, Борис!'
// Через 2 секунды: 'Привет, Вера!'
Каждый вызов setTimeout захватывает свою копию переменной name из соответствующей итерации. Без замыканий реализовать подобное поведение было бы значительно сложнее.
Замыкания в циклах: почему возникают ошибки и как их исправлять
Работа внутри циклов — одна из классических ловушек JavaScript, с которой сталкивается практически каждый разработчик. Проблема возникает из-за того, как переменные цикла взаимодействуют с этим механизмом, и понимание критически важно для написания корректного кода.
Проблема с var в цикле for
Рассмотрим типичную ситуацию: нам нужно создать несколько обработчиков событий в цикле. Вот код, который, казалось бы, должен работать, но ведёт себя неожиданно:
const helpText = [
{ id: 'email', help: 'Введите ваш email' },
{ id: 'name', help: 'Введите ваше имя' },
{ id: 'age', help: 'Введите ваш возраст' }
];
function setupHelp() {
for (var i = 0; i < helpText.length; i++) {
const item = helpText[i];
document.getElementById(item.id).onfocus = function() {
console.log(item.help);
};
}
}
На первый взгляд всё логично: при фокусе на каждом поле должна выводиться соответствующая подсказка. Однако в реальности все три обработчика выведут последнюю подсказку — «Введите ваш возраст». Почему так происходит?
Причина кроется в том, что var создаёт переменную с функциональной областью видимости. Это означает, что переменная i существует в единственном экземпляре на всю setupHelp, а не создаётся заново на каждой итерации. Когда цикл завершается, i содержит значение 3, и все замыкания ссылаются на одну и ту же переменную с этим финальным значением. Переменная item хоть и объявлена через const, но она также пересоздаётся на каждой итерации с новой ссылкой на элемент массива — и все обработчики в итоге ссылаются на последний item.

Из-за функциональной области видимости var, все функции, созданные в цикле, ссылаются на одну и ту же переменную i. В результате все они используют её последнее значение (в данном примере — 3).
Способ №1: использование IIFE
До появления let в ES6 проблему решали с помощью немедленно вызываемого функционального выражения (IIFE — Immediately Invoked Function Expression). Идея заключается в создании новой области видимости на каждой итерации:
function setupHelp() {
for (var i = 0; i < helpText.length; i++) {
(function() {
const item = helpText[i];
document.getElementById(item.id).onfocus = function() {
console.log(item.help);
};
})();
}
}
Или более элегантный вариант с передачей параметра:
function setupHelp() {
for (var i = 0; i < helpText.length; i++) {
(function(index) {
const item = helpText[index];
document.getElementById(item.id).onfocus = function() {
console.log(item.help);
};
})(i);
}
}
IIFE создаёт новый контекст выполнения на каждой итерации, и переменная index (или item) фиксируется в своём замыкании. Это работает, но код выглядит громоздко и требует понимания дополнительной концепции.
Способ №2: использование let
Современное и рекомендуемое решение — использование let вместо var. Ключевое слово let создаёт блочную область видимости, что означает создание новой переменной на каждой итерации цикла:
function setupHelp() {
for (let i = 0; i < helpText.length; i++) {
const item = helpText[i];
document.getElementById(item.id).onfocus = function() {
console.log(item.help);
};
}
}
Теперь каждая итерация цикла получает собственную копию переменной i, и замыкание корректно захватывает нужное значение. Код становится чище и понятнее.
Альтернативный подход — использование forEach, где каждая итерация автоматически создаёт новый контекст:
function setupHelp() {
helpText.forEach(item => {
document.getElementById(item.id).onfocus = function() {
console.log(item.help);
};
});
}
Сравним три подхода:
| Подход | Читаемость | Поддержка браузеров | Рекомендация |
| var + IIFE | Низкая (громоздко) | Отличная (все браузеры) | Устаревший подход |
| let в цикле | Высокая (лаконично) | ES6+ (современные браузеры) | Рекомендуется |
| forEach | Высокая (декларативно) | Хорошая (ES5+) | Рекомендуется для итераций по массиву |
В современной разработке стоит отдавать предпочтение let или методам массивов вроде forEach, map, filter — они создают естественные границы областей видимости и делают код более предсказуемым.
Частые ошибки при работе с замыканиями и как их избежать
Этот механизм — мощный инструмент, но при неправильном использовании может привести к трудноуловимым багам и проблемам с производительностью. Рассмотрим наиболее распространённые ошибки и способы их предотвращения.
Избыточные замыкания в циклах
Одна из типичных ошибок — создание функций с замыканиями внутри цикла без реальной необходимости. Каждая такая занимает память, и если цикл выполняется тысячи раз, это может привести к значительным накладным расходам. Если не требуется для захвата уникального состояния каждой итерации, лучше вынести наружу:
// Плохо: создаём новую на каждой итерации
items.forEach(item => {
item.addEventListener('click', function() {
console.log('Клик по элементу');
});
});
// Хорошо: переиспользуем одну
function handleClick() {
console.log('Клик по элементу');
}
items.forEach(item => {
item.addEventListener('click', handleClick);
});
Замыкание на тяжёлые объекты и утечки памяти
Они сохраняют ссылки на все переменные из внешней области видимости, даже если не используются внутри. Это может привести к тому, что большие структуры данных или DOM-элементы останутся в памяти дольше необходимого:
// Проблемный код
function processLargeData() {
const hugeArray = new Array(1000000).fill('data');
const smallValue = hugeArray[0];
return function() {
console.log(smallValue); // Нужно только одно значение
// Но весь hugeArray остаётся в памяти из-за замыкания!
};
}
// Решение: явно освобождаем ненужные данные
function processLargeData() {
const hugeArray = new Array(1000000).fill('data');
const smallValue = hugeArray[0];
// hugeArray больше не нужен, но JS может не понять этого
return function() {
console.log(smallValue);
};
}
// Лучшее решение: не захватываем лишнее
function processLargeData() {
const smallValue = new Array(1000000).fill('data')[0];
return function() {
console.log(smallValue);
};
}
Проблемы с асинхронностью
При работе с асинхронными операциями внутри циклов часто возникает ситуация, когда все коллбэки используют одно и то же значение переменной:
// Ошибка: все таймеры выведут 5
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, i * 1000);
}
// Решение 1: использовать let
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, i * 1000);
}
// Решение 2: явно захватить значение через параметр
for (var i = 0; i < 5; i++) {
setTimeout(function(index) {
console.log(index);
}, i * 1000, i);
}
Вечные ссылки на DOM-элементы
Особенно опасная ситуация возникает, когда замыкание сохраняет ссылку на DOM-элемент, который уже удалён из документа. Элемент остаётся в памяти, так как на него есть ссылка:
// Проблемный код
function attachHandler() {
const button = document.getElementById('myButton');
const largeData = { /* большой объект данных */ };
button.addEventListener('click', function() {
console.log('Клик!');
// Замыкание захватывает и button, и largeData
});
}
// Если button удалён из DOM, но обработчик не снят,
// элемент и данные остаются в памяти
// Решение: копировать только нужные данные
function attachHandler() {
const button = document.getElementById('myButton');
const buttonText = button.textContent; // Примитив вместо ссылки
button.addEventListener('click', function() {
console.log(`Клик! Текст был: ${buttonText}`);
});
// И не забывать снимать обработчики при удалении элементов
}
Рекомендации по предотвращению ошибок:
- Следите за тем, какие переменные захватывает замыкание — не должно быть лишних тяжёлых объектов.
- Всегда используйте let или const вместо var для переменных цикла.
- Снимайте обработчики событий при удалении DOM-элементов через removeEventListener.
- Если не нужно, выносите функцию за пределы цикла или области, где создаются переменные.
- Используйте инструменты профилирования (например, Chrome DevTools Memory Profiler) для обнаружения утечек памяти.
- При работе с асинхронностью проверяйте, что захватывает именно те значения, которые вы ожидаете.
Понимание этих подводных камней позволит писать более надёжный и производительный код, избегая типичных проблем, связанных с неправильным использованием.
Продвинутые паттерны и полезные техники на замыканиях
Они лежат в основе множества элегантных паттернов проектирования в JavaScript. Рассмотрим наиболее востребованные техники, которые активно применяются в современной разработке.
Модульный паттерн (Module Pattern)
Один из классических способов организации кода, позволяющий создавать модули с публичным API и приватными данными. До появления ES6-модулей это был стандартный подход к инкапсуляции:
const Calculator = (function() {
// Приватные переменные и методы
let memory = 0;
function validateNumber(num) {
return typeof num === 'number' && !isNaN(num);
}
// Публичный API
return {
add: function(a, b) {
if (validateNumber(a) && validateNumber(b)) {
return a + b;
}
throw new Error('Некорректные аргументы');
},
memorySave: function(value) {
if (validateNumber(value)) {
memory = value;
}
},
memoryRecall: function() {
return memory;
}
};
})();
Calculator.add(5, 3); // 8
Calculator.memorySave(42);
Calculator.memoryRecall(); // 42
Модульный паттерн создаёт единственный экземпляр с инкапсулированным состоянием, что удобно для утилит и сервисов.
Частичное применение (Partial Application)
Техника, позволяющая создавать новые замыкания путём фиксации части аргументов исходной:
function partial(fn, ...fixedArgs) {
return function(...remainingArgs) {
return fn(...fixedArgs, ...remainingArgs);
};
}
function multiply(a, b, c) {
return a * b * c;
}
const double = partial(multiply, 2);
console.log(double(3, 4)); // 24 (2 * 3 * 4)
const triple = partial(multiply, 3);
console.log(triple(2, 5)); // 30 (3 * 2 * 5)
Частичное применение особенно полезно при работе с функциями высшего порядка, когда нужно адаптировать сигнатуру под конкретный контекст использования.
Каррирование (Currying)
Преобразование с несколькими аргументами в последовательность, каждая из которых принимает один аргумент:
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function(...nextArgs) {
return curried.apply(this, args.concat(nextArgs));
};
}
};
}
function sum(a, b, c) {
return a + b + c;
}
const curriedSum = curry(sum); console.log(curriedSum(1)(2)(3)); // 6 console.log(curriedSum(1, 2)(3)); // 6 console.log(curriedSum(1)(2, 3)); // 6
Каррирование делает замыкания более гибкими и композируемыми, что активно используется в функциональном программировании и библиотеках вроде Ramda или Lodash.
Эмуляция приватных полей до появления #private
До того как в JavaScript появился синтаксис приватных полей через #, замыкания были единственным надёжным способом создания истинно приватных данных в классах:
function BankAccount(initialBalance) {
// Приватные данные через замыкание
let balance = initialBalance;
const transactions = [];
// Приватный метод
function recordTransaction(type, amount) {
transactions.push({
type,
amount,
date: new Date(),
balance: balance
});
}
// Публичные методы через прототип не получится,
// поэтому возвращаем объект
this.deposit = function(amount) {
if (amount > 0) {
balance += amount;
recordTransaction('deposit', amount);
return true;
}
return false;
};
this.withdraw = function(amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
recordTransaction('withdraw', amount);
return true;
}
return false;
};
this.getBalance = function() {
return balance;
};
this.getHistory = function() {
return [...transactions]; // Возвращаем копию
};
}
const account = new BankAccount(1000);
account.deposit(500);
account.withdraw(200);
console.log(account.getBalance()); // 1300
console.log(account.balance); // undefined - приватная переменная
Сводная таблица паттернов:
| Паттерн | Описание | Где пригодится |
| Module Pattern | Инкапсуляция с публичным API | Создание библиотек, утилит, синглтонов |
| Partial Application | Фиксация части аргументов | Адаптация, создание специализированных версий |
| Currying | Преобразование в цепочку с одним аргументом | Функциональное программирование, композиция |
| Private Fields Emulation | Скрытие данных через замыкания | Классы с истинной инкапсуляцией (до ES2022) |
Эти паттерны демонстрируют, насколько гибким инструментом являются замыкания. Они позволяют реализовывать сложные архитектурные решения, сохраняя при этом чистоту и выразительность кода. В современной разработке многие из этих техник встроены во фреймворки и библиотеки, но понимание их внутреннего устройства делает вас более компетентным разработчиком.
Замыкания и производительность: когда они вредят коду
Это не бесплатная абстракция. Каждое требует выделения памяти для хранения лексического окружения и всех захваченных переменных. В большинстве случаев эти затраты незначительны, но при неосторожном использовании могут возникнуть проблемы с производительностью.
Как движок хранит их в памяти. Когда создаётся замыкание, JavaScript-движок сохраняет ссылку на лексическое окружение, содержащее все переменные из внешней области видимости. Это означает, что даже если функция использует только одну переменную из внешней области, всё окружение остаётся в памяти до тех пор, пока существует хотя бы одна ссылка. Современные движки (V8, SpiderMonkey) оптимизируют этот процесс, сохраняя только действительно используемые переменные, но полагаться исключительно на оптимизации не стоит.
Какие занимают много памяти. Наиболее проблемные сценарии возникают, когда замыкание захватывает:
- Большие массивы или объекты с данными (особенно если нужна лишь малая часть этих данных).
- DOM-элементы, которые впоследствии удаляются из документа, но продолжают жить в памяти.
- Результаты тяжёлых вычислений или загруженные файлы, которые больше не актуальны.
- Вложенные, где каждый уровень захватывает данные предыдущего — это создаёт длинные цепочки ссылок.
Особенно опасна ситуация, когда создаются тысячи в цикле, и каждое замыкание захватывает объёмные данные:
// Проблемный код: утечка памяти
function createHandlers(items) {
const largeDataSet = fetchHugeData(); // Несколько мегабайт данных
items.forEach(item => {
item.onclick = function() {
// Используем только item, но largeDataSet тоже захватывается
processItem(item);
};
});
}
Когда лучше отказаться
Существуют ситуации, где они создают избыточную нагрузку:
- Высокочастотные операции. Если функция вызывается тысячи раз в секунду (например, в обработчиках scroll или mousemove), накладные расходы на создание и сборку мусора могут стать заметными. В таких случаях лучше использовать обычные или методы объектов.
- Работа с огромными коллекциями. При обработке массивов из десятков тысяч элементов, где каждому назначается обработчик с замыканием, суммарное потребление памяти может быть существенным. Рассмотрите делегирование событий или хранение данных в атрибутах элементов.
- Долгоживущие приложения. В SPA (Single Page Applications), работающих часами без перезагрузки, важно следить за тем, чтобы старые корректно освобождались при навигации между страницами или удалении компонентов.
Практические рекомендации:
- Используйте инструменты профилирования (Chrome DevTools → Memory → Heap Snapshot) для выявления утечек памяти.
- Явно обнуляйте ссылки на тяжёлые объекты, когда они больше не нужны.
- Предпочитайте делегирование событий массовому созданию обработчиков.
- В критичных по производительности участках кода рассмотрите альтернативы: классы с методами, WeakMap для хранения приватных данных.
- Снимайте обработчики событий и таймеры при уничтожении компонентов или элементов.
Это мощный инструмент, но, как и любая абстракция, требует осознанного применения. В подавляющем большинстве случаев их влияние на производительность пренебрежимо мало, но знание потенциальных проблем позволяет избежать неприятных сюрпризов в продакшене.
Заключение
Давайте систематизируем ключевые тезисы, которые необходимо усвоить для уверенной работы с замыканиями в JavaScript:
- Замыкания в JavaScript позволяют функциям сохранять доступ к переменным из внешнего лексического окружения. Это делает возможным хранение состояния и работу с данными после завершения внешней функции.
- Механизм замыканий основан на лексическом окружении и цепочке областей видимости. Именно статическая область видимости определяет, какие переменные будут доступны функции.
- Замыкания широко применяются на практике: для приватных переменных, фабрик функций, обработчиков событий и модульных паттернов. Они лежат в основе многих архитектурных решений.
- Ошибки при работе с замыканиями чаще всего связаны с циклами, использованием var и асинхронностью. Понимание областей видимости помогает избежать неожиданных багов.
- Неправильное использование замыканий может приводить к утечкам памяти и проблемам с производительностью. Особенно важно контролировать захват тяжёлых объектов и DOM-элементов.
- Осознанная работа с замыканиями повышает качество и надёжность JavaScript-кода. Этот механизм остаётся фундаментальным даже при использовании современных фреймворков.
Если вы только начинаете осваивать профессию frontend-разработчика и хотите глубже разобраться в JavaScript, рекомендуем обратить внимание на подборку курсов по JavaScript. В них сочетаются теоретическая база и практическая часть, что помогает быстрее понять сложные темы, включая замыкания, и применять их в реальных задачах.
Рекомендуем посмотреть курсы по JavaScript разработке
| Курс | Школа | Цена | Рассрочка | Длительность | Дата начала | Ссылка на курс |
|---|---|---|---|---|---|---|
|
Fullstack-разработчик на JavaScript
|
Eduson Academy
100 отзывов
|
Цена
Ещё -5% по промокоду
147 000 ₽
|
От
12 250 ₽/мес
0% на 24 месяца
|
Длительность
9 месяцев
|
Старт
в любое время
|
Подробнее |
|
Автоматизированное тестирование веб-приложений на JavaScript
|
Skillbox
218 отзывов
|
Цена
Ещё -47% по промокоду
48 408 ₽
64 548 ₽
|
От
4 034 ₽/мес
Без переплат на 1 год.
5 379 ₽/мес
|
Длительность
4 месяца
|
Старт
30 января
|
Подробнее |
|
Полный курс по JavaScript — С нуля до результата!
|
Stepik
33 отзыва
|
Цена
2 990 ₽
|
От
748 ₽/мес
|
Длительность
1 неделя
|
Старт
в любое время
|
Подробнее |
|
Backend-разработка на Node.js
|
Нетология
46 отзывов
|
Цена
с промокодом kursy-online
28 500 ₽
50 000 ₽
|
От
2 500 ₽/мес
Без переплат на 1 год.
|
Длительность
6 месяцев
|
Старт
в любое время
|
Подробнее |
|
Профессия Fullstack-разработчик на Python
|
Skillbox
218 отзывов
|
Цена
Ещё -20% по промокоду
146 073 ₽
292 147 ₽
|
От
4 296 ₽/мес
|
Длительность
12 месяцев
|
Старт
30 января
|
Подробнее |
Стратегия автоматизации тестирования: этапы успеха
Хотите, чтобы автоматизация тестирования приносила реальную пользу, а не становилась тратой времени? Расскажем о каждом этапе стратегии и поделимся
Точно в цель: чем полезна programmatic-реклама и кому она нужна
Что такое programmatic реклама, как она меняет подход к продвижению и стоит ли использовать её в 2025 году — разбираемся на простых примерах и с реальными кейсами.
Что такое модуль pickle в Python
Как сериализовать нейросеть, структуру графа или сложный объект в Python? И почему pickle может быть опаснее, чем кажется? Объясняем доступно и с кодом.
Названы самые популярные языки программирования в феврале 2025 года
TIOBE обновил рейтинг языков программирования на февраль 2025 года. Узнайте, какие языки стали лидерами и какие потеряли позиции.