Как сравнить массивы аргументов для кеширования в функции memo в JavaScript
Что проверяют
Знаете ли вы, почему сравнение делают поверхностным, и понимаете ли разницу между === и Object.is.
Как отвечать
Стандартное решение это поверхностное сравнение: сначала длины, затем каждый элемент по отдельности.
function sameArgs(a, b) {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (!Object.is(a[i], b[i])) return false;
}
return true;
}Почему именно Object.is, а не ===: он корректно обрабатывает два особых случая. NaN === NaN даёт false, из-за чего кэш сбрасывался бы при одинаковых аргументах, а Object.is(NaN, NaN) даёт true. И наоборот, +0 === -0 даёт true, хотя это разные значения. Именно Object.is использует React в своих сравнениях.
Почему не сериализация в строку, хотя это первое, что приходит в голову:
- медленно: на больших объектах преобразование дороже самой мемоизируемой функции;
- зависит от порядка ключей: два одинаковых по смыслу объекта дадут разные строки;
- теряет данные: функции,
undefinedи символы просто исчезают, а даты превращаются в строки. Разные аргументы могут дать одинаковый ключ.
Ограничение поверхностного сравнения назвать обязательно: объекты сравниваются по ссылке. Новый литерал или новая стрелочная функция на каждом вызове гарантированно промахиваются мимо кэша. Лечится стабилизацией самих аргументов выше по коду, а не усложнением сравнения.
Глубокое сравнение возможно, но оно стоит времени, пропорционального размеру данных, и часто съедает весь выигрыш от кэша. Его берут осознанно, когда вычисление действительно дорогое.
Отдельный приём: если ключом служит объект, удобно хранить кэш в WeakMap. Тогда запись исчезает вместе с объектом и утечки не возникает.
Где спотыкаются
Сравнивают через сериализацию в строку.
Используют === и теряют кэш на NaN.
Делают глубокое сравнение там, где выигрыш меньше его стоимости.
Не замечают, что каждый вызов приходит с новым объектом, и кэш не срабатывает никогда.