Back

Как работают замыкания в JavaScript

Как работают замыкания в JavaScript

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

Эта статья объясняет, как работают замыкания в JavaScript, что на самом деле означает лексическая область видимости и как избежать распространённых ошибок, возникающих из-за непонимания этих механизмов.

Ключевые моменты

  • Замыкание — это функция в сочетании с её лексическим окружением — привязками переменных, которые существовали на момент создания функции.
  • Замыкания захватывают привязки (ссылки), а не значения, поэтому изменения замкнутых переменных остаются видимыми.
  • Используйте let или const в циклах для создания свежих привязок на каждой итерации и избежания классической проблемы с циклами.
  • Проблемы с памятью возникают из-за сохранения ненужных ссылок, а не из-за самих замыканий.

Что такое замыкание?

Замыкание — это функция в сочетании с её лексическим окружением — набором привязок переменных, которые существовали на момент создания функции. Каждая функция в JavaScript формирует замыкание в момент создания.

function createGreeter(greeting) {
  return function(name) {
    return `${greeting}, ${name}`
  }
}

const sayHello = createGreeter('Hello')
sayHello('Alice') // "Hello, Alice"

Когда createGreeter возвращает результат, её контекст выполнения завершается. Тем не менее, возвращённая функция всё ещё имеет доступ к greeting. Внутренняя функция не скопировала строку “Hello” — она сохранила ссылку на саму привязку.

Лексическая область видимости в JavaScript

Лексическая область видимости означает, что доступ к переменным определяется тем, где функции определены в исходном коде, а не где они выполняются. Движок JavaScript разрешает имена переменных, проходя по цепочке областей видимости от самой внутренней области наружу.

const multiplier = 2

function outer() {
  const multiplier = 10
  
  function inner(value) {
    return value * multiplier
  }
  
  return inner
}

const calculate = outer()
calculate(5) // 50, а не 10

Функция inner использует multiplier из своего лексического окружения — области видимости, где она была определена — независимо от любой глобальной переменной с тем же именем.

Замыкания захватывают привязки, а не снимки

Распространённое заблуждение заключается в том, что замыкания «замораживают» значения переменных. Это не так. Замыкания хранят ссылки на привязки, поэтому изменения остаются видимыми:

function createCounter() {
  let count = 0
  return {
    increment() { count++ },
    getValue() { return count }
  }
}

const counter = createCounter()
counter.increment()
counter.increment()
counter.getValue() // 2

Оба метода разделяют одну и ту же привязку count. Изменения, внесённые increment, видны для getValue, потому что они ссылаются на одну и ту же переменную.

Классическая проблема с циклами: var против let

Это различие наиболее важно в циклах. С var все итерации разделяют одну привязку:

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100)
}
// Выводит: 3, 3, 3

Каждый callback замыкается на одном и том же i, который равен 3 к моменту выполнения callback’ов.

С let каждая итерация создаёт свежую привязку:

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100)
}
// Выводит: 0, 1, 2

Блочная область видимости даёт каждому замыканию собственный i.

Практические паттерны: фабричные функции и обработчики событий

Замыкания позволяют создавать фабричные функции, которые производят специализированное поведение:

function createValidator(minLength) {
  return function(input) {
    return input.length >= minLength
  }
}

const validatePassword = createValidator(8)
validatePassword('secret') // false
validatePassword('longenough') // true

Обработчики событий естественным образом используют замыкания для сохранения контекста:

function setupButton(buttonId, message) {
  document.getElementById(buttonId).addEventListener('click', () => {
    console.log(message)
  })
}

Callback сохраняет доступ к message ещё долго после того, как setupButton вернёт результат.

Вопросы памяти: что на самом деле сохраняют замыкания

Замыкания не вызывают утечек памяти по своей природе. Проблемы возникают, когда функции непреднамеренно сохраняют ссылки на большие объекты или долгоживущие структуры данных.

function processData(largeDataset) {
  const summary = computeSummary(largeDataset)
  
  return function() {
    return summary // Сохраняет только summary, а не largeDataset
  }
}

Если вашему замыканию нужна только небольшая часть данных, извлеките её перед созданием внутренней функции. Исходный большой объект станет доступным для сборки мусора.

Современные движки JavaScript агрессивно оптимизируют замыкания. Это нормальная языковая особенность, а не проблема производительности при типичном использовании. Проблема не в самих замыканиях — а в сохранении ссылок, которые вам не нужны.

Построение надёжной ментальной модели

Думайте о замыканиях следующим образом: когда создаётся функция, она захватывает ссылку на своё окружающее пространство. Это пространство содержит привязки — связи между именами и значениями — а не сами значения. Функция может читать и изменять эти привязки на протяжении всего своего жизненного цикла.

Эта модель объясняет, почему изменения видны, почему let решает проблемы с циклами и почему замыкания работают через асинхронные границы. Функция и её лексическое окружение путешествуют вместе.

Заключение

Замыкания — это функции, объединённые со своим лексическим окружением. Они захватывают привязки, а не значения, поэтому изменения замкнутых переменных остаются видимыми. Используйте let или const в циклах для создания свежих привязок на каждой итерации. Проблемы с памятью возникают из-за сохранения ненужных ссылок, а не из-за самих замыканий.

Понимание области видимости и замыканий в JavaScript даёт вам основу для рассуждений о времени жизни переменных, инкапсуляции данных и поведении callback’ов — паттернах, с которыми вы будете сталкиваться ежедневно во frontend-разработке.

Часто задаваемые вопросы

Каждая функция в JavaScript технически является замыканием, потому что она захватывает своё лексическое окружение в момент создания. Термин замыкание обычно относится к функциям, которые обращаются к переменным из внешней области видимости после того, как эта область завершила выполнение. Функция, которая использует только свои локальные переменные или глобальные переменные, не демонстрирует поведение замыкания значимым образом.

Замыкания не вызывают утечек памяти по своей природе. Проблемы возникают, когда замыкания непреднамеренно сохраняют ссылки на большие объекты или структуры данных, которые должны быть собраны сборщиком мусора. Чтобы избежать этого, извлекайте только те данные, которые нужны вашему замыканию, перед созданием внутренней функции. Современные движки JavaScript агрессивно оптимизируют замыкания, поэтому они не являются проблемой производительности при типичном использовании.

Это происходит при использовании var в циклах, потому что var имеет функциональную область видимости, а не блочную. Все итерации разделяют одну и ту же привязку, поэтому callback'и видят финальное значение при выполнении. Исправьте это, используя let вместо var, который создаёт свежую привязку для каждой итерации. Каждое замыкание тогда захватывает собственную копию переменной цикла.

Да. Замыкания захватывают привязки (ссылки на переменные), а не снимки значений. Если замкнутая переменная изменяется, замыкание видит обновлённое значение. Вот почему несколько функций, разделяющих одно замыкание, могут общаться через общие переменные, как видно в паттерне счётчика, где методы increment и getValue разделяют одну и ту же привязку count.

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Check our GitHub repo and join the thousands of developers in our community.

OpenReplay