Замыкание в JavaScript

Замыкание в JavaScript

Почему тема замыканий сложная

Замыкание сложно, потому что нужно одновременно держать в голове:

  • где функция объявлена;
  • где она вызвана;
  • какие переменные она "видит" по лексической области видимости;
  • сколько живет это состояние после завершения внешней функции.

Ключевая мысль: замыкание - это не отдельный синтаксис, а поведение обычных функций в JavaScript.

Шаг 1: лексическая область видимости

Функция ищет переменные не по месту вызова, а по месту объявления.

const level = 'global';

function outer() {
  const level = 'outer';

  function inner() {
    console.log(level);
  }

  inner();
}

outer(); // outer

inner берет level из outer, потому что объявлена внутри outer.

Шаг 2: что такое замыкание

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

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

const hi = createGreeter('Привет');
console.log(hi('Анна')); // Привет, Анна

prefix продолжает жить внутри возвращенной функции hi.

Смотри, что важно: замыкание "захватывает" переменную, а не копирует ее значение один раз. Если переменная меняется, внутренняя функция будет видеть новое значение:

let x = 1;

function makeLogger() {
  return function () {
    console.log(x);
  };
}

const logX = makeLogger();
x = 2;
logX(); // 2

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

function createCounter() {
  let count = 0;

  return function () {
    count += 1;
    return count;
  };
}

const next = createCounter();
console.log(next()); // 1
console.log(next()); // 2

Переменная count приватная: снаружи к ней нет прямого доступа, но замыкание ее сохраняет между вызовами.

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

function createUserState() {
  let isOnline = false;

  return {
    setOnline() {
      isOnline = true;
    },
    setOffline() {
      isOnline = false;
    },
    getStatus() {
      return isOnline;
    },
  };
}

Это базовый паттерн инкапсуляции: состояние есть, но управляется только через API.

Фабрики функций (параметризация логики)

function createMinValidator(min) {
  return function (value) {
    return value >= min;
  };
}

const isAdult = createMinValidator(18);
console.log(isAdult(17)); // false
console.log(isAdult(20)); // true

Каждая созданная функция хранит свой min.

Важно: разделяемое и изолированное состояние

Если использовать одно и то же замыкание - состояние общее:

const counter = createCounter();
const a = counter;
const b = counter;

console.log(a()); // 1
console.log(b()); // 2

Если вызвать фабрику два раза - состояние изолировано:

const c1 = createCounter();
const c2 = createCounter();

console.log(c1()); // 1
console.log(c1()); // 2
console.log(c2()); // 1

Это одна из самых важных идей для UI-компонентов и сервисов.

Частая ловушка: var в цикле

const fns = [];

for (var i = 0; i < 3; i++) {
  fns.push(function () {
    console.log(i);
  });
}

fns[0](); // 3
fns[1](); // 3
fns[2](); // 3

С var одна общая переменная i на весь цикл, и к моменту вызова она уже равна 3.

Правильный вариант для новичка - let:

const fns2 = [];

for (let j = 0; j < 3; j++) {
  fns2.push(function () {
    console.log(j);
  });
}

fns2[0](); // 0
fns2[1](); // 1
fns2[2](); // 2

С let на каждой итерации создается новая переменная j.

Замыкание в асинхронном коде

function createDelayedLogger(label) {
  return function () {
    setTimeout(() => {
      console.log(label);
    }, 100);
  };
}

const logSave = createDelayedLogger('saved');
logSave(); // через ~100мс: saved

label сохраняется в замыкании до момента выполнения setTimeout.

Где применять замыкания осознанно

  • приватное состояние без глобальных переменных;
  • фабрики функций (createValidator, createFormatter);
  • настройка обработчиков с контекстом;
  • утилиты вроде debounce/throttle.

Проверь себя: почему замыкание удобнее глобальной переменной для счетчика конкретного компонента?

Частые ошибки новичков

  • думать, что замыкание копирует значение один раз и "замораживает" его;
  • не понимать, делится ли состояние между ссылками;
  • использовать var в циклах с отложенными вызовами;
  • хранить в замыкании слишком большие данные без необходимости.

Анти-провал: всегда отвечай на 2 вопроса: "кто владеет состоянием?" и "когда это состояние должно умереть?".

Краткий итог

  • Замыкание - это функция + доступ к внешним переменным из места объявления.
  • Внешние переменные могут жить дольше внешней функции, если их использует внутренняя.
  • Замыкания дают приватность и переиспользуемые фабрики поведения.
  • Один вызов фабрики = одно состояние, если не делишь одну и ту же ссылку.
  • Понимание var/let и жизненного цикла переменных критично для корректной работы замыканий.