Перейти к содержанию

Замыкания и время жизни данных

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

Снимок advanced/lesson-34 создаёт независимые хранилища избранного. Не используем браузерное постоянное хранилище: здесь речь о времени жизни данных в памяти. Программа ещё не запускалась; ожидаемый результат показывает изоляцию экземпляров и сохранение доступа между вызовами.

Локальная переменная после возврата

Полная фабрика состояния:

export function createFavoriteStore(initial = []) {
  const ids = new Set(initial);
  return {
    toggle(id) {
      if (ids.has(id)) ids.delete(id);
      else ids.add(id);
      return ids.has(id);
    },
    list() { return [...ids]; },
  };
}

Локальный ids создаётся при вызове фабрики. Возвращённые методы используют его, поэтому нужная среда остаётся доступной вместе с функциями. После возврата мы не читаем ids по имени снаружи, но можем управлять состоянием через известные операции toggle и list.

Это не копия набора, сделанная в момент возврата каждой функции. Оба метода одного объекта имеют доступ к одному локальному набору. Поэтому действие toggle влияет на следующий результат list. Если бы каждый метод создавал новый Set, такое сохранение между действиями исчезло бы.

Основной механизм лексического окружения и замыканий объясняется в MDN о closures. Он относится к расположению определений в программе, а не к тому, кто случайно вызвал метод. Это отличает доступ к ids от динамического this прошлого урока.

Независимость созданных состояний

Добавим два вызова фабрики:

const first = createFavoriteStore();
const second = createFavoriteStore();
first.toggle('javascript');
console.log(first.list());
console.log(second.list());

Ожидается один идентификатор в первом списке и пустой второй список. Каждый вызов фабрики создал собственную локальную переменную. Текст функции одинаков, но окружения отдельных вызовов разные. Это удобно для двух независимых виджетов на странице.

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

Исходный массив initial используется для создания нового набора. Дальнейшее добавление строки в массив не изменяет уже созданный Set. Если элементы являются объектами, их ссылочная природа сохраняется; здесь храним строки идентификаторов и избегаем этой неоднозначности.

Удобная граница доступа

Метод list возвращает новый массив строк. Читатель операции может изменить полученный массив, не удаляя значения внутреннего набора. Это сохраняет управление через известные методы и не отдаёт наружу сам изменяемый Set.

Такой приём называют сокрытием реализации, но не следует превращать его в обещание безопасности секретов браузера. Данные находятся в клиентской программе, а разработчик может изучать среду. Граница прежде всего предотвращает случайные несогласованные изменения из других частей собственного кода.

toggle принимает идентификатор и возвращает текущее состояние после операции. Первый вызов для JavaScript вернёт истину, второй ложь. Он не создаёт DOM и не пишет в сеть. Отображение кнопки может использовать результат, но остаётся другой обязанностью.

В постоянном приложении вход id следует сверять с моделью курсов. Наша фабрика является универсальным маленьким набором строк и не знает источник каталога. Обработчик 26 уже проверял существование идентификатора до изменения избранного. Эти границы можно соединить без дублирования всего поведения внутри фабрики.

Обработчики и жизнь страницы

Когда возвращённый метод передан слушателю, слушатель удерживает ссылку на функцию, а функция — доступ к нужной среде. Поэтому удаление одного DOM-элемента не является универсальным обещанием мгновенного освобождения всех данных. Если глобальный слушатель продолжает ссылаться на callback, связанное состояние может оставаться доступным.

Для компонента важно иметь операцию отключения. В следующем классе слушатели будут объединены сигналом жизненного цикла и удаляться при unmount. Это организация ссылок и обязанностей, а не попытка вручную командовать сборщиком мусора.

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

Замыкание в цикле

Если создаются несколько функций внутри обхода, каждая читает переменные своего окружения. Современные блочные объявления помогают не спутать одну общую переменную с отдельными значениями итераций. В наших первых уроках for…of использовал const для текущего курса, поэтому callback может ясно связываться с конкретным идентификатором.

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

Излишне широкая внешняя область тоже неудобна. Если callback случайно обращается к большому объекту приложения только ради одного id, разбор его зависимостей усложняется. Передавайте и сохраняйте то, что нужно операции. Не называйте это измеренной оптимизацией без профилирования; это ясность модели ссылок.

Проверяемые следствия

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

Ссылки, возвращаемые из замыкания

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

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

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

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

Для наблюдения получите массив из list, измените его локально и вызовите list заново. Ожидается неизменный внутренний выбор. Затем переключите курс через публичную toggle и сравните два независимых хранилища. Такое разделение показывает, где проходят разрешённые изменения. Закрытая область хранит временное состояние экземпляра; она по-прежнему не записывает данные на диск и не синхронизирует их с другим документом.

Также попробуйте фабрику с начальным Markdown-идентификатором. Ожидается один элемент до действий, а переключение JavaScript добавит второй. Ни один такой опыт не выполнялся при подготовке. Дальше оформим управление каталога классом: состояние станет полями экземпляра, а прототип объяснит совместное использование методов.

Оглавление курса