Замыкания и область видимости

Замыкания и область видимости #

Замыкание — функция, которая «помнит» переменные из внешней области видимости даже после того, как внешняя функция уже завершилась. В 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, но идея «закрытого» состояния та же.


Частые ошибки #

  1. Утечка памяти — замыкание держит ссылку на большой объект, который больше не нужен.
  2. var в асинхронных колбэках — все итерации делят одну переменную.
  3. Создание функции в горячем цикле без необходимости — лишние аллокации; вынесите фабрику наружу.
  4. Ожидание «свежих» глобальных — замыкание фиксирует ссылку на объект, а не снимок примитива; мутация объекта снаружи видна внутри.
const config = { theme: 'light' }

function getTheme() {
  return config.theme
}

config.theme = 'dark'
getTheme() // "dark" — ссылка на тот же объект

Итог #

  • Замыкание — функция плюс лексическое окружение, в котором она родилась.
  • Внутренние функции читают переменные внешних даже после их завершения.
  • let в циклах создаёт отдельное замыкание на итерацию; var — нет.
  • Замыкания дают приватное состояние, фабрики функций и основу модульного стиля.

См. также #