Замыкания и область видимости #
Замыкание — функция, которая «помнит» переменные из внешней области видимости даже после того, как внешняя функция уже завершилась. В JavaScript это обычное поведение: каждая функция создаётся вместе со ссылкой на окружение, в котором она была объявлена.
Лексическая область видимости #
JavaScript ищет переменные снаружи внутрь — от текущего блока к родительским:
const courseTag = 'jsflow'
function describeLesson(title) {
const prefix = '[урок]'
return function format() {
return `${prefix} ${courseTag}: ${title}`
}
}
const formatIntro = describeLesson('Введение')
formatIntro() // "[урок] jsflow: Введение"
Внутренняя format видит prefix из describeLesson и courseTag из глобальной области. Это и есть лексическая (статическая) видимость: она определяется местом объявления, а не местом вызова.
Замыкание сохраняет состояние #
Внешняя функция может вернуть внутреннюю, а та продолжит работать с «замороженными» значениями:
function createProgressTracker(totalLessons) {
let completed = 0
return {
markDone() {
if (completed < totalLessons) completed += 1
return completed
},
getPercent() {
return Math.round((completed / totalLessons) * 100)
},
}
}
const tracker = createProgressTracker(5)
tracker.markDone() // 1
tracker.markDone() // 2
tracker.getPercent() // 40
Переменная completed не доступна снаружи напрямую, но методы объекта к ней обращаются — приватное состояние через замыкание.
Замыкания в циклах #
Классическая ловушка — var в цикле с отложенным вызовом:
// плохо: все колбэки увидят одно и то же i === 3
for (var i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i)
}, 100)
}
// 3, 3, 3
var одна на весь цикл; к моменту вызова колбэка цикл уже закончился. Решения:
// let — новая привязка на каждой итерации
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100)
}
// 0, 1, 2
// или IIFE с параметром
for (var i = 0; i < 3; i++) {
;(function (step) {
setTimeout(() => console.log(step), 100)
})(i)
}
Практический паттерн: фабрика валидаторов #
Замыкания удобны, когда нужна настраиваемая логика без классов:
function createMinLengthValidator(min) {
return function validate(text) {
return text.length >= min
}
}
const validateTitle = createMinLengthValidator(3)
const validateBio = createMinLengthValidator(20)
validateTitle('JS') // false
validateTitle('JSFlow') // true
validateBio('Коротко') // false
Каждый валидатор хранит свой min в замыкании.
Модули через замыкания #
До ES-модулей приватность часто строили на IIFE и замыканиях:
const lessonStore = (function () {
let cache = []
return {
add(lesson) {
cache.push(lesson)
},
count() {
return cache.length
},
list() {
return [...cache]
},
}
})()
lessonStore.add({ id: 1, title: 'Замыкания' })
lessonStore.count() // 1
// cache снаружи недоступен
Современный код предпочитает import/export, но идея «закрытого» состояния та же.
Частые ошибки #
- Утечка памяти — замыкание держит ссылку на большой объект, который больше не нужен.
varв асинхронных колбэках — все итерации делят одну переменную.- Создание функции в горячем цикле без необходимости — лишние аллокации; вынесите фабрику наружу.
- Ожидание «свежих» глобальных — замыкание фиксирует ссылку на объект, а не снимок примитива; мутация объекта снаружи видна внутри.
const config = { theme: 'light' }
function getTheme() {
return config.theme
}
config.theme = 'dark'
getTheme() // "dark" — ссылка на тот же объект
Итог #
- Замыкание — функция плюс лексическое окружение, в котором она родилась.
- Внутренние функции читают переменные внешних даже после их завершения.
letв циклах создаёт отдельное замыкание на итерацию;var— нет.- Замыкания дают приватное состояние, фабрики функций и основу модульного стиля.