ЯдроКодаподготовка к экзаменам
Учебная платформа

Загружаем материалы

Подготавливаем материалы и навигацию по разделу.

Замыкания JavaScript: состояние функции, области видимости и время жизни

Автор: · Обновлено

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

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

Среда и задача замыкания

Сохраните основной пример в closures.mjs и выполните node closures.mjs в Node.js 22 или новее. Он работает и в консоли браузера. Нужны функции, возврат значений, объекты и сравнения. Пока не требуются таймеры и Promise: время жизни переменной можно понять на полностью синхронном примере, не смешивая его с порядком асинхронных событий.

Представьте два учебных плана. Каждый хранит своё число завершённых заданий. Общая переменная перепутает их прогресс, а постоянная передача значения туда и обратно усложнит интерфейс. Фабрика может создать отдельное состояние и вернуть маленький набор действий над ним. Внешний код получает право отмечать задания и читать результат, но не получает прямое имя внутренней переменной.

Полный пример: независимые планы

function createProgress(limit) {
  if (!Number.isInteger(limit) || limit < 0) {
    throw new RangeError('Invalid limit');
  }
  let completed = 0;
  return {
    complete() {
      if (completed < limit) completed += 1;
      return completed;
    },
    snapshot() {
      return { completed, limit };
    },
  };
}
const first = createProgress(2);
const second = createProgress(5);
first.complete();
first.complete();
first.complete();
console.log(JSON.stringify(first.snapshot()));
console.log(JSON.stringify(second.snapshot()));
const detached = first.complete;
console.log(detached());

Ожидаются {"completed":2,"limit":2}, {"completed":0,"limit":5} и 2. Каждый вызов фабрики создал собственные переменные. Методы первого объекта разделяют одно состояние между собой; методы второго — другое. Отсоединённый метод продолжает работать, поскольку обращается к лексической переменной completed, а не к свойству this.completed. Это пригодится при сравнении замыканий с контекстом вызова в следующем уроке.

Слово «лексический» означает, что доступные имена определяются местом объявления функции в исходном коде. Вызов из другого файла или вложенной функции не заменяет это окружение. Если вызывающая функция имеет собственную переменную completed, она не становится состоянием нашего метода. При поиске имени учитывается ближайшая подходящая область вокруг определения функции, а не случайная цепочка её вызовов.

Ссылка на переменную, а не фотография значения

Полезно различать переменную и вычисленное из неё значение. Замыкание читает доступную ему переменную в момент вызова. Если она изменилась, функция увидит новое значение. Но строка, вычисленная заранее, сама по себе не пересобирается. Такой «устаревший снимок» возникает и без фреймворков: программа сохранила результат выражения вместо функции, вычисляющей актуальное состояние.

let score = 1;
const frozenLabel = 'Баллы: ' + score;
const readScore = () => score;
const readLabel = () => frozenLabel;
score = 4;
console.log(readScore());
console.log(readLabel());

Получатся 4 и Баллы: 1. Оба замыкания работают правильно: они читают разные переменные. Чтобы подпись отражала актуальный результат, формируйте её внутри функции из текущего score. Если же нужен снимок на момент отправки формы, сохранённое значение может быть правильным решением. Проблема определяется требованиями, а не самим фактом использования замыкания.

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

Циклы и несколько обработчиков

Классическая ошибка появляется, когда несколько функций читают одну изменяемую переменную цикла. С var такой счётчик принадлежит функции, а не отдельному проходу. К моменту позднего вызова обработчиков цикл уже закончен, и все читают финальное значение. Это не означает, что JavaScript запустил цикл задом наперёд или потерял порядок callback.

С let в заголовке обычного for создаются подходящие отдельные привязки для итераций. Другой понятный вариант — фабрика, которая получает текущее значение и возвращает обработчик. Важно понимать, что именно сохраняется: независимое число, общий объект или вычисленный снимок строки. Замена ключевого слова может исправить один случай, но не заменяет анализа данных.

В отладчике остановитесь внутри возвращённой функции и раскройте раздел областей видимости. Сравните локальные переменные, параметры и внешние привязки. Если имя затенено более близкой переменной, функция читает именно её. Попытка прочитать let до инициализации приводит к ReferenceError; наличие замыкания не отменяет временную мёртвую зону. При этом создать функцию до инициализации и вызвать её после допустимо.

Практика: фабрика ограниченного журнала

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

function createRecentLog() {
  const entries = [];
  return {
    add(message) {
      entries.push(message);
      if (entries.length > 2) entries.shift();
    },
    read() { return [...entries]; },
  };
}
const log = createRecentLog();
log.add('открыт');
log.add('изменён');
log.add('сохранён');
const external = log.read();
external.length = 0;
console.log(log.read().join(','));

Ожидаемый вывод: изменён,сохранён. Если вернуть сам entries, очистка external очистит журнал. Если записи станут объектами, копирование только массива уже не защитит вложенные поля: контракт потребуется пересмотреть. Это показывает, как замыкания и модель ссылок дополняют друг друга, а не решают одну задачу взаимозаменяемыми способами.

Частые вопросы

Освободится ли состояние после return фабрики?

Не обязательно: доступные извне функции ещё могут обращаться к нему. Сборщик памяти учитывает достижимость. После удаления всех нужных ссылок данные могут стать кандидатами на освобождение, но точный момент не гарантируется.

Замыкание само является утечкой?

Нет. Сохранение состояния часто и есть цель. Ошибка возникает, если долгоживущий обработчик или глобальный список удерживает больше данных или дольше, чем требуется. Удаляйте подписки и ограничивайте накопительные структуры по их жизненному циклу.

Это способ хранить секреты от пользователя браузера?

Нет. Инкапсуляция помогает организовать программу, но пользователь контролирует свою среду и инструменты отладки. Серверные секреты не должны попадать в клиентский код, независимо от количества вложенных функций.

Источники