Avatar ·

Как работают замыкания (closures) в JavaScript? Объясните на примерах

Всем привет! Я только начал изучать JavaScript и никак не могу понять тему замыканий (closures). Читал много статей, но всё равно путаюсь. Вот пример, который меня смущает. Я создаю функцию, которая возвращает другую функцию: ```javascript function createCounter() { let count = 0; return function() { count++; return count; }; } const counter1 = createCounter(); const counter2 = createCounter(); console.log(counter1()); // 1 console.log(counter1()); // 2 console.log(counter2()); // 1 — почему не 3? ``` Почему `counter1` и `counter2` ведут себя как отдельные счётчики? Я думал, что переменная `count` будет общей. И как вообще JavaScript "помнит" значение `count` после того, как функция `createCounter` завершила свою работу? Ведь обычно локальные переменные уничтожаются после выхода из функции. Объясните, пожалуйста, механизм работы замыканий простыми словами. Как они хранят ссылки на переменные? И где это может пригодиться в реальных проектах? Буду благодарен за понятные примеры! <<>>

Что такое замыкание и как оно работает

Замыкание (closure) — это комбинация функции и её лексического окружения (scope), в котором эта функция была создана. Простыми словами: функция "запоминает" переменные из внешней функции, даже после того, как внешняя функция завершила выполнение.

Разбор вашего примера

Давайте разберём, что происходит в коде шаг за шагом:

function createCounter() {
  let count = 0;          // (1) Локальная переменная
  return function() {     // (2) Внутренняя функция
    count++;
    return count;
  };
}

const counter1 = createCounter(); // (3)
const counter2 = createCounter(); // (4)

Шаг 1: Когда вызывается createCounter(), создаётся новое лексическое окружение (назовём его Env1). В нём объявляется переменная count = 0.

Шаг 2: Внутренняя функция (анонимная) создаётся внутри Env1. Она получает скрытое свойство [[Environment]], которое ссылается на Env1. Это и есть механизм замыкания — функция "запоминает" окружение, в котором родилась.

Шаг 3: counter1 получает ссылку на эту внутреннюю функцию. Внешняя функция createCounter завершает работу, но Env1 не уничтожается, потому что на него всё ещё ссылается counter1 через [[Environment]].

Шаг 4: При вызове counter1() создаётся новое временное окружение для этого вызова. JavaScript ищет переменную count: сначала в локальном окружении (не находит), затем переходит в [[Environment]] — то есть в Env1 — и находит её там. count увеличивается до 1.

Шаг 5: При вызове createCounter() во второй раз (для counter2) создаётся совершенно новое лексическое окружение Env2 с новой переменной count = 0. counter2 ссылается на Env2, а не на Env1.

Поэтому counter1 и counter2 — это независимые счётчики, каждый со своим "личным" count.

Как JavaScript "помнит" переменные?

Ответ: через скрытое свойство [[Environment]]. Когда функция создаётся, движок JavaScript сохраняет в этом свойстве ссылку на текущее лексическое окружение. При вызове функции движок использует эту ссылку для поиска переменных, которые не найдены в локальном окружении.

Практические примеры использования замыканий

1. Приватные переменные (инкапсуляция)

function createUser(name) {
  let _password = '';
  return {
    getName: () => name,
    setPassword: (pwd) => { _password = pwd; },
    checkPassword: (pwd) => _password === pwd
  };
}

const user = createUser('Alice');
user.setPassword('12345');
console.log(user.checkPassword('12345')); // true
console.log(user._password); // undefined — нет прямого доступа

2. Функциональное программирование (каррирование)

function multiply(a) {
  return function(b) {
    return a * b;
  };
}

const double = multiply(2);
const triple = multiply(3);

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

3. Обработчики событий с сохранением состояния

function createButtonLogger(buttonName) {
  let clickCount = 0;
  return function() {
    clickCount++;
    console.log(`Кнопка "${buttonName}" нажата ${clickCount} раз(а)`);
  };
}

const logSave = createButtonLogger('Сохранить');
const logDelete = createButtonLogger('Удалить');

// Прикрепить к кнопкам:
// saveBtn.addEventListener('click', logSave);
// deleteBtn.addEventListener('click', logDelete);

Важные моменты

  • Каждый вызов внешней функции создаёт новое замыкание с собственным набором переменных.
  • Замыкание хранит ссылку на переменные, а не их копии. Поэтому если изменить переменную изнутри замыкания, это изменение будет видно всем функциям, которые разделяют это окружение.
  • Замыкания могут привести к утечкам памяти, если случайно сохранить ссылку на большое замыкание, которое больше не нужно. В современных браузерах это редкость, но стоит помнить.
  • Классическая ошибка с замыканиями в циклах решается с помощью let (создаёт новую переменную на каждой итерации) или через немедленно вызываемую функцию (IIFE).
<<>>

Другой взгляд: замыкание как "рюкзак" с переменными

Представьте, что каждая функция — это человек, который идёт в поход. Когда он создаётся (определяется), он берёт с собой "рюкзак" — лексическое окружение, в котором он был создан. В этом рюкзаке лежат все переменные, которые были доступны в момент создания функции.

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

Пример с setTimeout (частая ошибка)

// Проблема: все выведут 3
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 1000);
}

// Решение через замыкание (var)
for (var i = 0; i < 3; i++) {
  (function(j) {
    setTimeout(() => console.log(j), 1000);
  })(i);
}

// Или проще — используйте let
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 1000); // 0, 1, 2
}

В первом случае все три setTimeout видят один и тот же i (который к моменту срабатывания таймеров уже стал равен 3). Во втором случае мы создаём новое замыкание на каждой итерации с помощью IIFE, и каждый таймер получает свою копию j. С let это происходит автоматически.

Краткое резюме

  • Замыкание = функция + её лексическое окружение на момент создания.
  • Позволяет функциям "помнить" переменные из внешней области видимости.
  • Каждый вызов внешней функции создаёт новое замыкание.
  • Используется для инкапсуляции, каррирования, создания фабрик функций и в асинхронном коде.