Замыкания в JavaScript: практика и типичные ошибки

Замыкания (Closures) — это одна из самых мощных и одновременно запутанных концепций в JavaScript. Простыми словами, замыкание — это функция, которая запоминает переменные из места своего создания, даже если она вызывается в другом месте. Это открывает возможности для инкапсуляции, но может приводить к утечкам памяти.

Как работает область видимости

Когда функция создается внутри другой функции, она имеет доступ к переменным внешней функции. Это связывание сохраняется даже после того, как внешняя функция завершила выполнение.

function createCounter() {
  let count = 0; // Приватная переменная

  return {
    increment: () => count++,
    decrement: () => count--,
    getCount: () => count
  };
}

const counter = createCounter();
console.log(counter.getCount()); // 0
counter.increment();
console.log(counter.getCount()); // 1
// Переменная count недоступна извне напрямую

В этом примере count защищен от прямого изменения из глобальной области видимости. Это паттерн модуля, реализованный через замыкание.

Замыкания в циклах: классическая проблема

До появления let разработчики часто сталкивались с проблемой замыканий в циклах var. Переменная var имеет функциональную область видимости, а не блочную.

// Ошибка с var
for (var i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i); // Выведет 3, 3, 3
  }, 100);
}

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

Использование let создает новую блочную область видимости для каждой итерации цикла, поэтому замыкание «захватывает» уникальное значение i для каждого таймера.

Память и утечки

Замыкания хранят ссылки на переменные внешней области. Если замыкание живет долго (например, висит в глобальном объекте или слушателе событий), оно удерживает в памяти все переменные, которые были доступны в момент его создания.

function setup() {
  const hugeData = new Array(1000000).fill('*');
  
  document.getElementById('btn').addEventListener('click', () => {
    console.log('Click'); 
    // hugeData удерживается в памяти, хотя не используется
  });
}

Чтобы избежать утечек, обнуляйте ссылки на большие объекты, если они не нужны внутри замыкания, или удаляйте слушатели событий, когда компонент уничтожается.