Замыкания в JavaScript
- среда, 1 апреля 2026 г. в 00:00:04
Замыкание это важный механизм JavaScript, понимание которого обязательно фронтендера. Он позволяет изящно реализовать принцип наименьшего раскрытия, благодаря инкапсулированию функций, сохраняя их состояние во внутренней области видимости, для последующего использования.
Замыкания лежать в основе функционального программирования и даже в основе ООП. Изначально они были математической концепцией из области лямбда-вычислений, получившей воплощение во многих языка программирования, особенно в функциональных Haskell, Lisp, ML и т.д.
В упрощённом виде, основные идеи лямбда-вычислений таковы:
Функции - это примитивная единицы вычисления. Все прочие данные кодируются в них;
Единственная задача функции, это применение к аргументу: принять аргумент, вернуть вычисленное с его участием значение;
Функции анонимны, их описывают лямбда-абстракции вида: λx. x + 5 означающие: "функция от x, возвращающая x + 5". Функция состоит из пары: параметры + тело;
Функция возвращающая или принимающая другую функцию, называется функцией высшего порядка.
Разработчик не сильно знакомый с математикой, сейчас такой: "Хмммм где-то я всё это слышал! Но где?!".

Теперь Вы знайте базу, о функциях лежащий в основе любого языка программирования и которые Вы каждый день пишите! Все их хорошо знакомые особенности, вышли из математической концепции, американского математика Алонзо Чёрча, разработанной в 30-х годах прошлого века.

Вернёмся к нашим баранам замыканиям. В теории лямбда-исчислений у функции есть окружение состоящее из свободных переменных, которая используется в функции, но не является её параметром и не объявлена внутри неё. Например в выражении λx. x + y:
x - параметр функции (связанная переменная);
y - свободная переменная, откуда-то из внешнего окружения.
Но что если наша лямбда функция λx. x + y попадёт в другой контекст, например станет частью функции высшего порядка? Как быть со свободной переменной y? Тут то на сцене и появляется замыкание, фиксирующее значение свободных переменных, в момент создания функции. Теперь наша функция будет вести себя стабильно и предсказуемо в любом контексте.
Для возникновения замыкания, функцию нужно вызвать, не в той области видимости, в которой она была объявлена. Реализуем пример, рассмотренную выше лямбда функцию на JS:
function counter() { let count = 0 // Свободная переменная для внутренней функции // Функция лямбда-абстракция return function(step) { count = count + step return count } } const myCounter = counter() // Замыкание состоящее из внутренней функции и её лексического окружения в виде переменной count console.log(myCounter(1)) // 1 console.log(myCounter(1)) // 2 console.log(myCounter(1)) // 3
Обязательным условием создания замыкания является наличие внешней функции, во внутренней области видимости. В примере выше, внутренняя функция замыкается на внешнюю переменную count, что не даёт сборщику мусора уничтожить данные хранящиеся в ней, даже после того как внешняя область видимости завершила свою работу.
Замыкания представляет собой живую ссылку, с доступом к полноценной переменной, а не моментальный снимок. Поэтому оно не ограничено только чтением данных, но может изменять её значение. Представление о том что замыкания сохраняют значения ошибочно, они сохраняют состояние переменной.
Замыкания являются частной особенностью функций и ничего другого. Концептуально замыкания работают на уровне областей видимости, а не переменных. По сути это пара: функции + её лексическое окружение, а всё вместе это, состояние внутренней функции, которое сохраняется в замыкании.
С ООП замыкания связывает некоторые общие принципы. По сути в примере со счётчиком реализованным через замыкания:
Переменная count - это приватное поле;
Сама функция counter - это публичный интерфейс, работающий с приватным состоянием.
Это позволяет нам реализовать полноценный объект с приватным состоянием, при помощи замыканий:
function Counter(initialValue = 0) { let count = initialValue // Приватное свойство // Публичные методы return { increment: function(step = 1) { return count += step }, decrement: function(step = 1) { return count -= step }, getValue: function() { return count }, reset: function() { return count = initialValue } } } const myCounter = Counter(10) // Создаём экземляр объекта console.log(myCounter.getValue()) // 10 console.log(myCounter.increment(5)) // 15 console.log(myCounter.increment(2)) // 17 console.log(myCounter.decrement(3)) // 14 console.log(myCounter.reset()) // 10
Выше при помощи связки функция + замыкание мы создали настоящий класс, который в нативном виде выглядел бы так:
class Counter { #count #initValue constructor(initialValue = 0) { this.#count = initialValue this.#initValue = initialValue } increment(step = 1) { return this.#count += step } decrement(step = 1) { return this.#count -= step } getValue() { return this.#count } reset() { return this.#count = this.#initValue } } const myCounter = new Counter(10)
Матёрый фронтенедер посмотрев на пример с классом из функции, увидит в нём что-то знакомое и родное. Да это же типичный стор из Pinia или Vuex, где наш пример мог выглядеть так:
export const useCounterStore = defineStore('counter', () => { const count = ref(0) const initValue = 0 function increment(step) { count.value + step } function decrement(step) { count.value - step } function reset () { count.value = initValue } return {count, increment, decrement, reset} })
По сути Pinia использует замыкания для инкапсуляции приватного состояния стора внутри объекта синголтона. Вот так вот, мы за несколько шагов дошли от математической абстракции, практически столетней давности, до современного JS фреймворка и всё что их объединяет, это замыкания!
Замыкания в стрелочных функциях работает так-же как и в обычных:
const counter = () => { let count = 0 return (step) => { count = count + step return count } } const myCounter = counter() console.log(myCounter(1)) // 1 console.log(myCounter(1)) // 2 console.log(myCounter(1)) // 3
Появится ли замыкание в цикле for зависит как мы объявим итератор. Если через var, то замыкания не будет:
for (var i = 0; i < 3; i++) { setTimeout(function() { console.log(i) // 3, 3, 3 — а не 0, 1, 2 }, 100) }
В консоли мы увидим 3 3 3, так как на момент срабатывания setTimeout() цикл завершит свою работу и будет работать с итоговым значением переменной i.
Но вот если переменную объявить через let, то картина будет другой:
for (let i = 0; i < 3; i++) { setTimeout(function() { console.log(i) // 0, 1, 2 }, 100) }
let создаёт отдельное замыкание, со своим лексическим окружением, для каждой итерации. Поэтому каждая итерация ссылается на своё уникальное состояние. Тоже самое может случиться если навесить обработчик события используя var:
var buttons = document.querySelectorAll('div') for (var i = 0; i < buttons.length; i++) { buttons[i].addEventListener('click', function() { console.log('Нажата кнопка ' + i) }) }
В примере выше при клике по любому из дивов, всегда будет выводится одно и тоже сообщение: Нажата кнопка 4, так как 4 это последнее значение переменной на момент завершения работы цикла. А если поменять var, на let, то всё будет работать корректно и в консоль будет выводится индекс элемента по которому кликнул пользователь:
var buttons = document.querySelectorAll('div') for (let i = 0; i < buttons.length; i++) { buttons[i].addEventListener('click', function() { console.log('Нажата кнопка ' + i) }) }
Итераторы forEach, map, filter создают замыкания во время своей работы, поэтому примеры с таймером, показывают разные значения на каждой итерации:
[0, 1, 2].forEach(function(i) { setTimeout(function() { console.log(i) // 0, 1, 2 }, 100) }) [0, 1, 2].map(function(i) { setTimeout(function() { console.log(i); // 0, 1, 2 }, 100) }) [0, 1, 2].filter(function(i) { setTimeout(function() { console.log(i) // 0, 1, 2 }, 100) return true })
Тут объяснить такое поведение проще простого. Данные методы на каждой итерации вызывают переданную коллбэк функцию, со своим окружением в виде переменной i, на которую и замыкается коллбэк функция внутри setTimeout().
Пожалуй тут с замыкания встречаются чаще всего, так как в JavaScript коллбэки используются буквально на каждом шагу в связи с его асинхронной природой.
Замыкания легко встретить в Ajax-запросах:
function ajaxRequest(url) { let requestId = Math.random().toString(36).substring(2) // Генерим уникальный индетификатор для запроса fetch(url).then(response => { if (!response.ok) { throw new Error('Чёт пошло не так!') } return response.json() }).then(data => { console.log(`Запрос ${requestId} успешно завершён:`, data) // Стрелочная функция замыкает requestId }).catch(error => { console.log(`Запрос ${requestId} провалился:`, error.message) // И тут тоже замыкание }) } ajaxRequest('https://jsonplaceholder.typicode.com/todos/1') ajaxRequest('https://jsonplaceholder.typicode.com/todos/2')
В пример выше коллбэки в then() и catch() замыкаются на переменной requestId из внешней функции и поэтому когда с сервера приходит асинхронный ответ, в консоль выведется айдишник соответствующего запроса.
Напишем простой обработчик событий со своим лексическим окружением:
function clickSetup(elemId, message) { const elem = document.getElementById(elemId) elem.addEventListener('click', function() { console.log(message) }) } clickSetup('myElem', 'По элементу кликнули!')
В примере выше коллбэк функция передаваемая обработчику события, замыкается на аргументе message, в момент подписки на событие, выводя его значение при клике, происходящем уже после того как функция clickSetup() отработает.
Современные движки JavaScript исключают из области видимости замыкания, любые переменные к котором нет явного обращения. Делается это ради оптимизации, так как замыкания порождаемые функциями обратного вызова, используемыми в JS повсеместно, со временем начинали потреблять слишком много памяти. Это легко проверить запустив код с деббагером:
function greeting() { let hello = 'Привет' let userName = 'Васятка' setTimeout(function() { debugger console.log(hello) }, 1000) } greeting()
После запуска кода из примера, нужно перейти на вкладку Источники (бурж. Sources), далее справа открыть Области действия (бурж. Scope), затем открыть Замыкание (бурж. Closure) и там будет только переменная hello:

Так-же если перейти на вкладку Консоль (бурж. Console), и там ввести имя переменной hello, то в ответ консоль выведет её значение. Если попробовать проделать, тот-же самый трюк с переменной userName, то Вы получите ошибку: Uncaught ReferenceError: userName is not defined. Это говорит о том что она была удалена сборщиком мусора и больше не доступна в приложении.
Если внутренняя функция будет обращаться ко внешнем переменным, но сама не будет вызвана:
function greeting(studentName) { return () => { console.log(`Привет ${studentName}`) } } greeting('Васятка') // Здесь нет замыканий!
То замыкание создастся на короткое время, но сразу после того как функция greeting() отработает, ссылка на неё теряется и она съедается сборщиком мусора, а вместе с ней и лексическое окружение захваченное внутренней функцией. Пронаблюдать этот эффект не получится, так что можно сказать что в примере выше, нет замыканий с практической точки зрения, но есть замыкание с технической точки зрения.
Замыкания могут выстраиваться в целые цепочки:
function createCalculator(initValue) { let value = initValue function add(amount) { value += amount return value } function get() { return value } function apply(number) { if (arg) { return add(number) } else { return get() } } return apply } const calc = createCalculator(10) console.log(calc(5)) // 15 console.log(calc()) // 15 console.log(calc(3)) // 18
В примере выше внешняя функция createCalculator() создаёт лексическое окружение, с переменной value и возвращает внутреннюю функцию apply(). Сама функция apply() не имеет прямого доступа к переменной value, но вызывает другие внутренние функции, имеющие к ней доступ и таким образом получается цепочка замыканий.
Замыкания можно передавать в качестве аргумента как и любые другие значения:
function greeting(helloPhrase) { let userName = 'Васятка' return function() { console.log(`${helloPhrase}, ${userName}!`) } } function executeCallback(callback) { callback() } executeCallback(greeting('Hola')) // Передаём замыкание в другую функцию
При передаче замыкание сохранит скрытую ссылку, на исходную область видимости и своё лексическое окружение. По сути передаются эти ссылки, а не экземпляры вызываемых функций. Замыкание будет существовать, до тех пор пока существует, хоть одна ссылка на экземпляр функции создавшей замыкание. Поэтому тут нужно быть осторожным и следить чтобы в памяти не зависали большие объекты.
Напишем простенький модуль для работы с формой, основанный на замыканиях:
function createFormController(formName) { // Кэшируем все нужные элементы формы const form = document.querySelector(`[name="${formName}"]`) const name = form.querySelector('[name="name"]') const email = form.querySelector('[name="email"]') const button = form.querySelector('[type="submit"]') const message = form.querySelector('.message') let isSubmitting = false function showMessage(messageText, isError = false) { message.textContent = messageText message.style.display = 'block' if (isError && !message.classList.contains('message_error')) { message.classList.add('message_error') } else { message.classList.remove('message_error') } } function getData() { return JSON.stringify({ name: name.value, email: email.value }) } function reset() { name.value = '' email.value = '' message.style.display = 'none' } function setSubmitting(submitting) { isSubmitting = submitting if (button) { button.disabled = submitting button.value = submitting ? 'Отправка...' : 'Отправить' } } function validate() { // Простая валидация if (!name.value) { showMessage('Введите имя', true) return false } if (!email.value.includes('@')) { showMessage('Введите корректный email', true) return false } message.style.display = 'none' return true } form.addEventListener('submit', (event) => { event.preventDefault() if (!validate()) { return false } setSubmitting(true) fetch('https://jsonplaceholder.typicode.com/posts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: getData() }).then((result) => { setSubmitting(false) reset() if(result.ok) { showMessage('Форма отправлена!') } else { showMessage('Ошибка при отправке формы!') } }) }) } const contactForm = createFormController('callback')
Данный модуль отлично демонстрирует все преимущества которые даёт использование замыканий:
Кэширование. Поиск по DOM дереву это дорогая операция, наш модуль знает об этом, поэтому кэширует элементы формы внутри замыкания. Затем методы данного модуля используют эти ссылки, а не выполняют каждый раз поиск по дереву DOM, тем самым экономя ресурсы;
Инкапсуляция. Данный модуль предоставляет для пользования только один внешний интерфейс, в виде функции createFormController(). Вся остальная логика скрыта внутри него, что сильно повышает стабильность кода.
Декомпозиция. По факту наш импровизированный модуль, это обычная функция createFormController(). Но за счёт использования замыкании и внутренних функций, мы смогли избежать спагетти кода, разбив его на небольшие внутренние функции. Это сильно улучшает читаемость кода и позволяет соблюдать SRP (бурж. Single Responsibility Principle - Принцип единой ответственности). Вместо одной мега-функции полностью отвечающей за всё что происходит с формой, код разбит на множество компактных частей, каждая из которых выполняет, только одно небольшое действие.
Замыкания это важнейший механизм JavaScript, являющийся фундаментом для таких фич языка как: модули и асинхронность. Замыкания повсеместно используются в популярных библиотеках и фреймворках JS. Поэтому без понимания принципов их работы, невозможно писать эффективный и безопасный код на JavaScript. А о том насколько популярны вопросы, по замыканиям на собесах и говорить не приходится. В данной статья мы подробно изучили, от куда замыкания пришли в программирование и как они ведут себя в современном JavaScript.