Что такое замыкание (closure) в JavaScript?

«Что такое замыкание (closure) в JavaScript?» — вопрос из категории JavaScript, который задают на 24% собеседований AQA / Automation. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

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

Практический пример:

function createCounter() {
  let count = 0; // Приватная переменная, инкапсулированная замыканием

  return {
    increment: function() {
      count += 1;
      return count;
    },
    decrement: function() {
      count -= 1;
      return count;
    },
    getValue: function() {
      return count;
    }
  };
}

const myCounter = createCounter();
console.log(myCounter.getValue()); // 0
console.log(myCounter.increment()); // 1
console.log(myCounter.increment()); // 2
console.log(myCounter.decrement()); // 1
// Переменная `count` напрямую недоступна извне, защищена замыканием.

Ключевые аспекты и применение:

  1. Создание приватного состояния: Как показано в примере выше, замыкание позволяет эмулировать приватные поля, скрывая данные от глобальной области видимости.
  2. Фабрики функций и каррирование: Позволяет создавать специализированные функции на основе общего шаблона.
    function multiplier(factor) {
      return function(x) {
        return x * factor;
      };
    }
    const double = multiplier(2);
    const triple = multiplier(3);
    console.log(double(5)); // 10
    console.log(triple(5)); // 15
  3. Работа с асинхронным кодом и обработчиками событий: Частая причина появления замыканий — сохранение контекста переменной внутри колбэка.
    function setupButtons() {
      for (var i = 0; i < 3; i++) {
        // Использование let вместо var решает классическую проблему,
        // так как let создаёт новую привязку для каждой итерации.
        let buttonId = i;
        document.getElementById(`btn-${i}`).addEventListener('click', function() {
          console.log(`Нажата кнопка ${buttonId}`); // Замыкание захватывает buttonId
        });
      }
    }

Важное замечание: Замыкание хранит ссылку на внешнюю переменную, а не её значение на момент создания. Это может приводить к неочевидному поведению в циклах (с var, как в старом примере выше) и требует внимания к управлению памятью, так как ссылки могут препятствовать сборке мусора.