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

Замыкание — функция, которая запоминает и имеет доступ к переменным из своего лексического окружения, даже когда выполняется вне этого окружения.

Зачем нужно

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

Где используется

Приватные переменные, счётчики, каррирование, мемоизация, модульный паттерн, обработчики событий, callback-функции, debounce/throttle.

Предпосылки

Function Declaration, Function Expression, Переменные

Лексическое окружение

Каждая функция при создании получает ссылку на лексическое окружение (Lexical Environment), в котором она была определена:

function outer() {
  const message = 'Привет'; // переменная внешней функции

  function inner() {
    console.log(message); // доступ к переменной outer
  }

  return inner;
}

const fn = outer; // outer завершилась
fn; // "Привет" — message всё ещё доступна!

Что происходит

  1. outer создаёт переменную message и функцию inner
  2. inner «замыкает» переменную message (сохраняет ссылку на окружение)
  3. outer возвращает inner и завершается
  4. Но message НЕ уничтожается — на неё есть ссылка из inner
  5. Вызов fn читает message из замкнутого окружения

Практические примеры

Счётчик

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

  return {
    increment { return ++count; },
    decrement { return --count; },
    getCount { return count; },
    reset { count = initial; }
  };
}

const counter = createCounter(10);
console.log(counter.increment); // 11
console.log(counter.increment); // 12
console.log(counter.decrement); // 11
console.log(counter.getCount);  // 11

// count недоступен напрямую — это приватная переменная!
// console.log(counter.count); // undefined

Приватные переменные

function createBankAccount(initialBalance) {
  let balance = initialBalance;
  const history = ;

  function log(action, amount) {
    history.push({
      action,
      amount,
      balance,
      date: new Date.toISOString()
    });
  }

  return {
    deposit(amount) {
      if (amount <= 0) throw new Error('Сумма должна быть положительной');
      balance += amount;
      log('deposit', amount);
      return balance;
    },
    withdraw(amount) {
      if (amount > balance) throw new Error('Недостаточно средств');
      balance -= amount;
      log('withdraw', amount);
      return balance;
    },
    getBalance { return balance; },
    getHistory { return [...history]; } // копия, не оригинал
  };
}

const account = createBankAccount(1000);
account.deposit(500);    // 1500
account.withdraw(200);   // 1300
console.log(account.getBalance); // 1300
// balance и history недоступны извне

Фабрика функций

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

const helloGreeter = createGreeter('Привет');
const goodbyeGreeter = createGreeter('До свидания');

console.log(helloGreeter('Иван'));     // "Привет, Иван!"
console.log(goodbyeGreeter('Мария'));  // "До свидания, Мария!"

Мемоизация

function memoize(fn) {
  const cache = new Map();

  return function(...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) {
      console.log('Из кэша');
      return cache.get(key);
    }
    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

const expensiveCalc = memoize((n) => {
  console.log('Вычисляю...');
  return n * n;
});

expensiveCalc(5); // "Вычисляю..." → 25
expensiveCalc(5); // "Из кэша" → 25

Модульный паттерн

const Logger = (function {
  let logs = ;
  let level = 'info';

  function formatMessage(msg, lvl) {
    return `[${lvl.toUpperCase()}] ${new Date.toISOString()}: ${msg}`;
  }

  return {
    setLevel(newLevel) { level = newLevel; },
    log(msg) {
      const formatted = formatMessage(msg, level);
      logs.push(formatted);
      console.log(formatted);
    },
    getLogs { return [...logs]; },
    clear { logs = ; }
  };
});

Logger.log('Приложение запущено');
Logger.setLevel('error');
Logger.log('Что-то пошло не так');

Замыкания и циклы

Классическая ловушка с var

// Проблема: var имеет функциональную область видимости
for (var i = 0; i < 3; i++) {
  setTimeout( => console.log(i), 100);
}
// Вывод: 3, 3, 3 — все функции замкнулись на одну переменную i

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

// Решение 2: IIFE (до ES6)
for (var i = 0; i < 3; i++) {
  (function(j) {
    setTimeout( => console.log(j), 100);
  })(i);
}
// Вывод: 0, 1, 2

Замыкание и память

Замыкания удерживают ссылку на всё лексическое окружение:

function createHeavyClosure() {
  const largeData = new Array(1000000).fill('data');

  // Эта функция замыкает largeData, даже если не использует его
  return function {
    return 'Замыкание создано';
  };
}

// largeData останется в памяти, пока жива функция
const fn = createHeavyClosure;
// Для освобождения: fn = null;

Минимизация утечек

function createOptimized() {
  const largeData = new Array(1000000).fill('data');
  const summary = largeData.length; // вычислить нужное заранее

  // Замыкаем только summary, не largeData
  return function {
    return `Элементов: ${summary}`;
  };
  // largeData будет собран GC, т.к. нет ссылок
}

Замыкание и this

function Timer() {
  this.seconds = 0;

  // Стрелочная функция замыкает this из Timer
  setInterval(() => {
    this.seconds++;
  }, 1000);
}

// Альтернатива: сохранить this в переменную (до ES6)
function TimerOld() {
  const self = this; // замыкаем this
  this.seconds = 0;

  setInterval(function {
    self.seconds++; // используем замкнутый self
  }, 1000);
}

Частые ошибки

1. Замыкание в цикле с var

// См. раздел "Замыкания и циклы" выше
// Всегда используйте let в циклах с асинхронными операциями

2. Утечки памяти через замыкания

function setupHandler() {
  const element = document.getElementById('button');
  const heavyData = loadHugeDataset;

  element.addEventListener('click', () => {
    // Замыкает heavyData, даже если не нужна
    console.log('Клик!');
  });

  // Решение: обнулить ненужные ссылки
  // heavyData = null; // если данные больше не нужны
}

3. Ожидание копии вместо ссылки

function example() {
  let value = 1;

  const getValue = () => value;
  const setValue = (v) => { value = v; };

  setValue(42);
  console.log(getValue); // 42, а не 1!
  // Замыкание хранит ССЫЛКУ, не копию
}

Практика

  1. Создай счётчик с методами increment, decrement, reset
  2. Напиши фабрику функций createMultiplier(factor)fn(n) => n * factor
  3. Реализуй приватную переменную через замыкание
  4. Напиши функцию once(fn) — вызовет fn только один раз
  5. Исправь проблему с var в цикле тремя способами (let, IIFE, bind)

Связанные темы

Ресурсы


🎓 Источник: Функции, стрелочные функции, контексты, замыкания

  • 📅 2018-09-27 · YouTube · ID: pn5myCmpV2U
  • Тезисы:
    • Замыкание = контекст, который может хранить состояние
    • Контексты вложены как матрёшка: блок, for, if тоже создают контекст
    • В чистом ФП состояние замыкания не меняется
    • К значениям замыкания нет прямого доступа — console.log печатает function
    • Простейшее замыкание hash хранит data и counter; экспортируем внутреннюю лямбду
    • Рекурсивное замыкание-сумматор: (x) => (y) => add(x + y) для бесконечной цепочки
  • Цитата: «Контексты, в которых лежат функции и состояние, имеют свойство выходить из-под контроля программиста, и нужно как можно лучше за этим всем следить.»
  • Код:
    const hash = () => {
      const data = {};
      let counter = 0;
      return (key, value) => {
        data[key] = value;
        counter++;
        return data;
      };
    };
    

⚡ Источник: Замыкания с точки зрения официальной спецификации · AsForJS

  • 📅 2025-07-05 · YouTube · ID: RvYq-wt_GEU
  • Тезисы:
    • В спеке ECMAScript термина «closure» нет — это академический термин CS
    • Замыкание = процесс связывания окружений, не «функция из функции»
    • JS решает восходящую funarg-проблему (lexical/static scoping)
    • var лежит в Variable Environment, let/const — в Lexical Environment (разные!)
    • Замыкание возникает в момент определения функции, не возврата
    • Даже блок { let a } создаёт замыкание по спеке
    • V8 не сохраняет окружение, если оно не используется — нарушает спеку ради скорости
    • Стрелка => x — единственная настоящая константа в JS
  • Цитата: «Замыканием называется процесс, когда одно окружение связывается с другим окружением, что позволяет коду находить идентификаторы в своем или в окружении выше.»
  • Подробнее: Замыкания -- funarg-проблема