$ sudo teach IT
РАЗДЕЛ 6 · УРОК 6

Замыкания (Closures)

Замыкание — одно из самых мощных и поначалу загадочных понятий в JavaScript. Функция «помнит» переменные из той области, где она была создана — даже после того, как эта область «закрылась». Разберём как это работает и зачем это нужно.

⏱ ~30 минут 🧠 Средний уровень 🟡 JavaScript

🔄 Вспомним: функции видят переменные родителя

В предыдущем уроке мы узнали о цепочке областей видимости: внутренняя функция видит переменные внешней функции. Это и есть основа замыканий.

function outer() {
  let message = "Привет из outer!";

  function inner() {
    // inner видит переменную message из outer
    console.log(message);
  }

  inner(); // "Привет из outer!"
}

outer();

Это пока ничего удивительного. Но что если внешняя функция вернёт внутреннюю? И что если мы вызовем внутреннюю позже — когда outer уже давно завершила работу?

💡 Суть замыкания: функция «захватывает» окружение

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

function createGreeter() {
  let greeting = "Привет"; // эта переменная "захватится" замыканием

  function greet(name) {
    // greet запомнила greeting из createGreeter
    console.log(greeting + ", " + name + "!");
  }

  return greet; // возвращаем функцию, а не её вызов!
}

// createGreeter() выполнилась и завершилась.
// Но greet "унесла" с собой переменную greeting
let greetFn = createGreeter();

// Вызываем greet позже — greeting всё ещё доступна!
greetFn("Иван");   // "Привет, Иван!"
greetFn("Мария");  // "Привет, Мария!"

🎒 Метафора «рюкзака»: функция при создании «упаковывает» в рюкзак все переменные из своего окружения. Куда бы функцию ни передали — рюкзак всегда с ней. Это и есть замыкание.

Формальное определение: замыкание (closure) — это функция, которая имеет доступ к переменным из своей области видимости, а также из всех внешних областей видимости, в которых она была создана — даже после того, как эти внешние области завершили выполнение.

🔢 Классический пример: счётчик с приватным состоянием

Самый известный пример замыкания — функция-счётчик. Каждый вызов увеличивает счётчик. Но счётчик «спрятан» внутри — снаружи к нему не добраться и не изменить напрямую.

function createCounter() {
  let count = 0; // приватная переменная — снаружи недоступна

  function increment() {
    count++;
    console.log("Счётчик:", count);
  }

  return increment;
}

let counter = createCounter();
counter(); // Счётчик: 1
counter(); // Счётчик: 2
counter(); // Счётчик: 3

// Попытка получить count напрямую:
console.log(count); // ❌ ReferenceError: count is not defined

// Каждый вызов createCounter() создаёт НОВОЕ независимое замыкание:
let counter1 = createCounter();
let counter2 = createCounter();

counter1(); // Счётчик: 1
counter1(); // Счётчик: 2
counter2(); // Счётчик: 1 — свой независимый count!

🔐 Инкапсуляция данных: это мощный паттерн! Переменная count полностью защищена. Её нельзя случайно изменить из кода снаружи. Единственный способ изменить счётчик — через специально предоставленную функцию increment.

🎛️ Расширенный счётчик: объект с методами

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

function createCounter(startValue = 0) {
  let count = startValue;

  return {
    increment() {
      count++;
    },
    decrement() {
      count--;
    },
    reset() {
      count = startValue;
    },
    getValue() {
      return count;
    }
  };
}

let counter = createCounter(10);
counter.increment();
counter.increment();
counter.increment();
console.log(counter.getValue()); // 13

counter.decrement();
console.log(counter.getValue()); // 12

counter.reset();
console.log(counter.getValue()); // 10 (вернулись к startValue)

// count по-прежнему недоступен напрямую:
console.log(counter.count); // undefined — нет такого свойства!

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

🏭 Фабрика функций: makeMultiplier

Ещё один популярный паттерн с замыканиями — фабрика функций: функция, которая создаёт и возвращает другие функции, «настроенные» под конкретный параметр.

// Фабрика умножителей
function makeMultiplier(factor) {
  // factor "захватывается" замыканием
  return function(number) {
    return number * factor;
  };
}

// Создаём конкретные умножители:
let double = makeMultiplier(2);
let triple = makeMultiplier(3);
let tenTimes = makeMultiplier(10);

console.log(double(5));   // 10
console.log(triple(5));   // 15
console.log(tenTimes(5)); // 50

// Можно применять к массивам:
let numbers = [1, 2, 3, 4, 5];
console.log(numbers.map(double));   // [2, 4, 6, 8, 10]
console.log(numbers.map(triple));   // [3, 6, 9, 12, 15]
// Ещё пример: фабрика приветствий
function makeGreeter(language) {
  const greetings = {
    ru: "Привет",
    en: "Hello",
    de: "Hallo",
    es: "Hola"
  };

  let greeting = greetings[language] || "Hi";

  return function(name) {
    return greeting + ", " + name + "!";
  };
}

let greetRu = makeGreeter("ru");
let greetEn = makeGreeter("en");

console.log(greetRu("Иван")); // "Привет, Иван!"
console.log(greetEn("John")); // "Hello, John!"

🪤 Частая ошибка: замыкания в цикле с var

Это одна из самых известных ловушек JavaScript — часто встречается на собеседованиях. Если создавать замыкания в цикле с var, все они будут использовать одну и ту же переменную:

// ❌ ОШИБКА: var — одна переменная i для всех замыканий
for (var i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i); // все выведут 3, а не 0, 1, 2!
  }, 1000);
}
// Через 1 секунду: 3, 3, 3 — не то, что ожидали!

// ✅ РЕШЕНИЕ: let создаёт новую переменную для каждой итерации
for (let i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i); // 0, 1, 2 — правильно!
  }, 1000);
}
// Через 1 секунду: 0, 1, 2

С let на каждой итерации создаётся новая переменная i — каждый колбэк замыкает свою копию. С var все колбэки замыкают одну и ту же переменную i, которая к моменту вызова уже равна 3.

🌐 Где замыкания используются в реальном коде

⏱️
setTimeout / setInterval

Колбэк «помнит» переменные из момента создания — это замыкание.

🎯
Обработчики событий

Функция-обработчик «помнит» данные, которые были в момент её создания (например, id элемента).

📦
Модули

Паттерн модуля — приватные данные через замыкание, публичный API через возвращаемый объект.

🔄
Мемоизация

Кэширование результатов вычислений — хранится в переменной внутри замыкания.

// Мемоизация через замыкание:
function memoize(fn) {
  let cache = {}; // кэш "живёт" в замыкании

  return function(n) {
    if (n in cache) {
      console.log("Из кэша:", n);
      return cache[n];
    }
    let result = fn(n);
    cache[n] = result;
    return result;
  };
}

function slowSquare(n) {
  // Имитируем медленное вычисление
  return n * n;
}

let fastSquare = memoize(slowSquare);
fastSquare(5); // Вычисляет: 25
fastSquare(5); // Из кэша: 5 → 25 (не вычисляет снова)
fastSquare(7); // Вычисляет: 49

✅ Итоги урока

  • Замыкание — функция + переменные из внешней области видимости, «захваченные» при создании
  • Внешняя функция завершилась — переменные живут, пока существует ссылка на внутреннюю функцию
  • Паттерн инкапсуляции: приватные данные в замыкании, доступ только через публичные функции
  • Каждый вызов внешней функции создаёт новое независимое замыкание со своими переменными
  • Фабрика функций — создаёт «настроенные» функции через замыкание параметра
  • В цикле замыкания: var — общая переменная для всех, let — новая на каждой итерации
  • Реальное применение: setTimeout, обработчики событий, модули, мемоизация

В следующем уроке — рекурсия: функции, которые вызывают сами себя. Это ещё один мощный инструмент, особенно для работы с вложенными структурами. 🚀

Тест: Замыкания (Closures)

6 вопросов

Счётчик через замыкание

Напишите функцию createCounter, которая принимает начальное значение start (по умолчанию 0) и возвращает объект с двумя методами: increment() — увеличивает счётчик на 1 и возвращает новое значение; getValue() — возвращает текущее значение счётчика. Используйте замыкание для приватной переменной count.

Фабрика умножителей

Напишите функцию makeMultiplier, которая принимает число factor и возвращает новую функцию. Возвращённая функция принимает число и возвращает его произведение с factor. Используйте замыкание для сохранения factor.