Замыкания на практике: счётчики и приватность

замыкания≈ 7 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

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

Разделы темы:·

Обычная переменная внутри функции живёт ровно до конца вызова. Но если функция вернёт другую функцию, которая эту переменную читает, — переменная останется жить, а снаружи к ней не будет ни одного пути. Это и есть приватность: не соглашение «не трогайте», а физическая невозможность.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Обратите внимание на c.count: свойства с таким именем у объекта просто нет. Мы не прятали его подчёркиванием и не писали #private — переменная осталась в области видимости, которую объект не показывает.

Каждый вызов фабрики создаёт новое замыкание со своей переменной. Счётчики не мешают друг другу — это ключевое свойство, из-за которого приём вообще работает.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

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

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

double и triple — один и тот же код с разной памятью. Так удобно готовить форматтеры, валидаторы, логгеры: настройку передали один раз, дальше вызываете короткой функцией без лишних аргументов.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Замыкание — не магия. Функция просто помнит, где она родилась, и держит эту комнату открытой.

У var нет блочной области видимости: на весь цикл существует одна переменная, и все созданные функции запомнили именно её. У let на каждой итерации своя переменная — значит, и своё замыкание. Подробный разбор — в статье «Замыкания: как функция помнит своё окружение»; здесь ловушка рядом, чтобы её было видно на месте.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

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

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

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

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Промежуточные звенья вроде add10 — обычные значения: их можно передавать, складывать в массив, вызывать позже. Именно поэтому такой стиль удобен там, где функции передают друг другу: map, обработчики событий, конвейеры.

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

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

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

Чтобы кэшировать любую функцию, ключом берём все аргументы. store в параметрах по умолчанию создаётся один раз — при вызове memoize — и дальше живёт в замыкании обёртки.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Ключ через JSON.stringify прост, но не всесилен: объекты с разным порядком полей дадут разные ключи, а функции и undefined в аргументах потеряются. Для двух-трёх примитивов этого хватает.

Инициализация, отправка формы, показ подсказки — есть действия, которые обязаны случиться ровно один раз, сколько бы раз их ни попросили. Обёртка хранит в замыкании флаг и готовый результат.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Вместо отдельного флага мы используем саму переменную fn: после первого вызова в ней null, проверка if (fn) больше не проходит. Заодно исходная функция освобождается — ссылки на неё в замыкании не остаётся.

Второй вызов вернул «старт 1», а не «старт 2», и это не ошибка: аргументы второго вызова просто игнорируются. Если такое поведение вас не устраивает — верните undefined или бросьте ошибку, решение за вами.

Пока жива функция, живо и всё, до чего она может дотянуться. Обычно это ровно то, что нужно, — но иногда в памяти застревает мегабайт, который вам уже не нужен.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

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

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

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

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

1/3Что выведет код? function makeCounter() { let n = 0; return () => ++n; } const a = makeCounter(); const b = makeCounter(); a(); a(); console.log(a(), b());

2/3Что выведет цикл с var? const fns = []; for (var i = 0; i < 3; i++) fns.push(() => i); console.log(fns[0](), fns[1](), fns[2]());

3/3Сколько раз выполнится тело исходной функции? let n = 0; const init = once(() => { n += 1; return "ok"; }); init(); init(); init(); console.log(n);

Счётчик с приватным состоянием

Напишите фабрику makeCounter(start), которая возвращает объект с методами up(), down(), reset() и value(). Значение должно храниться так, чтобы снаружи его нельзя было прочитать или изменить напрямую — только через методы. Два разных счётчика не должны влиять друг на друга.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Переменные n и start остаются в области видимости фабрики: методы их видят, внешний код — нет. reset возвращает не ноль, а исходное значение, поэтому start тоже нужно запомнить.

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Ключевая проверка — c.n даёт undefined: у объекта такого свойства нет, значение живёт только в замыкании. Каждый вызов makeCounter создаёт свою переменную n, поэтому счётчики независимы.

Мемоизация с доказательством

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

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Кэш создаётся один раз внутри memoize и остаётся жить в замыкании возвращённой функции. Проверять наличие ключа нужно через has, а не через истинность значения: иначе закэшированные 0, "" или undefined будут считаться промахом и пересчитываться каждый раз.

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

calls равен двум — по одному настоящему вычислению на каждый новый аргумент. Помните про обратную сторону: этот Map будет расти, пока жива функция fast.