← Назад к списку
ПрограммированиеFrontendMiddle

Напишите функцию memoize, кэширующую результаты вызовов. Какие есть подводные камни у мемоизации?

Короткий ответ

  • Кэш в замыкании: Map ключ → результат
  • Ключ строится из аргументов: JSON.stringify или кастомный resolver
  • Повторный вызов с теми же аргументами возвращает кэш
  • Имеет смысл только для чистых функций
  • JSON.stringify не различает порядок ключей и функции
  • Кэш без ограничения размера — утечка памяти
  • Для объектов-аргументов подходит WeakMap

Memoize хранит результаты по ключу из аргументов в замыкании и возвращает кэш при повторном вызове; корректность требует чистоты функции, а продакшен-версия — продуманного ключа и ограничения размера кэша.

Как сказать вслух

пример ответа

Memoize оборачивает функцию и запоминает результаты: при повторном вызове с теми же аргументами возвращается сохранённое значение вместо пересчёта. Храню кэш в Map внутри замыкания, ключ собираю из аргументов. Важно понимать ограничения: это работает только для чистых функций — у которых результат зависит лишь от аргументов. И ключ из JSON.stringify — компромисс: он не справится с функциями в аргументах, а кэш без ограничения размера со временем съедает память, поэтому в проде добавляют вытеснение, например LRU.

Подробный ответ

Основной ответ

Базовая реализация: замыкание с Map, ключ — serialize(args), при промахе вызываем fn.apply(this, args) и кладём результат в кэш. Тонкие места, по которым оценивают кандидата. Ключ: JSON.stringify различает '1' и 1 хуже, чем кажется (ключ '["1"]' против '[1]' — тут как раз нормально), но не различает объекты с разным порядком ключей, теряет undefined и функции, падает на циклических структурах; поэтому хорошие реализации принимают resolver — функцию построения ключа. Память: кэш растёт бесконечно, в долгоживущем приложении это утечка — нужны ограничение размера и вытеснение (LRU) либо WeakMap для единственного объектного аргумента, позволяющий сборщику мусора забирать ключи. Семантика: мемоизация корректна только для чистых функций; кэшировать функцию с побочными эффектами или зависимостью от времени — баг. Для промисов отдельный вопрос: кэшировать ли отклонённые — обычно их из кэша удаляют.

Ключевые моменты

  • Чистота функции. Мемоизация предполагает детерминированность; для функций с эффектами она меняет поведение программы.
  • Построение ключа. JSON.stringify — компромисс; resolver с явным ключом надёжнее и быстрее.
  • Рост кэша. Без LRU или WeakMap кэш — управляемая утечка памяти в SPA, живущей часами.
  • Контекст. fn.apply(this, args) сохраняет работу мемоизированных методов объектов.

Практический контекст

Задача проверяет замыкания, работу с Map и инженерное мышление о краевых случаях. В повседневной работе идея та же, что в useMemo и React.memo в React или reselect в Redux: не пересчитывать дорогое при неизменных входах. Хороший кандидат сам проговаривает, когда мемоизация вредна — дешёвая функция плюс накладные расходы на ключ и память дают замедление вместо ускорения.

Пример кода

function memoize(fn, resolver) {
  const cache = new Map();
  return function (...args) {
    const key = resolver ? resolver(...args) : JSON.stringify(args);
    if (cache.has(key)) return cache.get(key);
    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

const slowSquare = memoize(n => { heavyWork(); return n * n; });
slowSquare(4); // вычислит
slowSquare(4); // вернёт из кэша

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

  • Мемоизируют нечистые функции и получают устаревшие результаты
  • Строят ключ через JSON.stringify и ловят коллизии на объектах с разным порядком ключей
  • Не ограничивают размер кэша в долгоживущем приложении

ИП Кочкин Алексей Сергеевич · ИНН 390509026279 · ОГРНИП 325390000030973 · jiniys2005@yandex.ru