Напишите функцию 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 и ловят коллизии на объектах с разным порядком ключей
- Не ограничивают размер кэша в долгоживущем приложении