Map и Set для данных интерфейса
Массив удобен для порядка карточек, но получение курса по постоянному идентификатору требует отдельного поиска. Избранное, наоборот, хранит уникальные идентификаторы без повторов. Map и Set выражают эти обязанности напрямую и помогают отделить идентичность записи от её позиции и отображаемого текста.
В снимке advanced/lesson-31 остаются четыре прежних курса. Создадим индекс по id, набор избранного и список уникальных тем. Пример не запускается при подготовке: приведённые результаты ожидаемые. Не объявляем замену массива автоматическим ускорением любого каталога без измерений и анализа операций.
Ключ и значение
Map сопоставляет ключ со значением. В нашем случае ключ — строковый id, значение — объект курса. Индекс создаётся один раз из исходного массива и затем используется для конкретного обращения:
export function indexCourses(rows) {
const index = new Map();
for (const course of rows) {
if (index.has(course.id)) {
throw new Error(`Повторный id: ${course.id}`);
}
index.set(course.id, course);
}
return index;
}
const byId = indexCourses(courses);
console.log(byId.get('javascript').title);
Ожидается название JavaScript-курса. Метод set записывает пару, get получает значение, has проверяет наличие. Эти операции выражают доступ по идентичности, а не перебор отображаемого порядка. Основное поведение описано в MDN о Map.
Повторный ключ обычно заменил бы прежнее значение. Для нашего каталога это скрывало бы повреждённые данные, поэтому индекс явно отклоняет повтор. Такая проверка совпадает с прежним контрактом validateCourses; она показывает обязанность построения индекса даже тогда, когда вход ещё не был проверен.
Сам индекс хранит ссылки на объекты массива. Изменение свойства через найденную запись изменит тот же объект. Поэтому сохраняем договор чтения, не рассматривая Map как глубокую копию. Порядок источника всё ещё принадлежит массиву, а индекс отвечает за получение по ключу.
Отсутствующий курс
Для неизвестного ключа get возвращает undefined. Перед чтением названия нужно решить, что означает отсутствие. Если избранное хранит курс, который исчез из каталога, это обычная ситуация устаревшей ссылки, а не повод создавать карточку с чужими полями.
const favoriteIds = new Set(['javascript', 'javascript', 'markdown']);
console.log(favoriteIds.size);
console.log([...favoriteIds].map(id =>
byId.get(id)?.title ?? 'Курс удалён'));
Ожидаемый размер набора равен двум. Повторный идентификатор не создаёт вторую запись. Подписи получают названия JavaScript и Markdown; неизвестное значение получило бы явный запасной текст. В реальном интерфейсе можно предложить удалить недоступное избранное, но это отдельная продуктовая операция.
has полезен, когда значение в общем Map само может быть undefined: тогда одно чтение не отличит отсутствующую пару от присутствующего значения. В нашей схеме значение всегда объект курса, поэтому отсутствие можно распознать непосредственно. Пример ограничен своим контрактом, а не универсальным правилом любого индекса.
Уникальный набор
Set хранит значения без повторов. Мы уже использовали его для проверки id и избранного, теперь рассмотрим выбор уникальных тем:
const topics = new Set(courses.map(course => course.topic));
console.log([...topics]);
Ожидаются frontend и publishing в порядке первого появления. Создание набора не сортирует значения по алфавиту и не формирует русские подписи. Если интерфейсу нужен иной порядок, он задаётся отдельно. Свойства набора описаны в MDN о Set.
При добавлении строки с другим регистром получится другое значение. Поэтому машинные темы и идентификаторы имеют заранее ограниченный формат. Не ожидайте, что Set сам выполнит нормализацию Unicode или преобразует подписи разных языков к одной теме.
Объекты сравниваются по идентичности. Два отдельно созданных объекта с одинаковым id будут двумя разными значениями Set. Для уникальности курсов по идентификатору храните именно идентификаторы либо стройте Map по ключу. Сама похожесть полей не является встроенным правилом сравнения объектов.
Что хранить постоянно
JSON напрямую не является специальным форматом сериализации Map и Set. Если просто передать набор в JSON.stringify, не получится ожидаемый массив выбранных идентификаторов. Сначала преобразуем его к согласованному обычному значению, например [...] со строками, затем проверяем при восстановлении.
Это связано с уроком хранилища: сохраняем данные договора, а не произвольные внутренние объекты реализации. Версия записи избранного могла бы содержать массив известных строковых id. При чтении проверяется формат, затем создаётся новый набор. Объекты DOM и функции туда не входят.
Не сохраняйте весь индекс курсов как независимую копию источника без необходимости. Тогда обновление каталога потребовало бы согласовать две версии данных. Индекс удобно построить из текущего проверенного массива после загрузки. Постоянные ссылки избранного могут жить отдельно, потому что выражают выбор читателя.
Выбор структуры по операции
Массив отвечает на вопрос порядка, Map — обращения по ключу, Set — уникальности. В одном интерфейсе они могут использоваться вместе без дублирования смысла. Это не соревнование, где одна коллекция обязана победить остальные. Для четырёх карточек простой поиск массива тоже является понятным решением.
Если данные обновились, прежний индекс не становится автоматически новым. Необходимо построить индекс из нового массива или выполнить явно определённое изменение. Так же удаление записи массива не удалит её пару из другого объекта Map само по себе. Производные структуры должны иметь понятный момент обновления.
Производный индекс и обновление источника
Предположим, что после загрузки получен новый объект JavaScript-курса с тем же id, но изменённым названием. Старый Map продолжает указывать на старый объект, если индекс не перестроен. Совпадение ключа не создаёт автоматическую связь между двумя независимо созданными записями. Поэтому принятие нового массива должно иметь понятный шаг построения производного индекса.
В небольшом приложении удобно создавать новый Map целиком после проверки нового набора. Это делает момент обновления явным и исключает забытые пары удалённых курсов. При частичных изменениях можно использовать set и delete, но тогда необходимо точно определить, какие операции над источником синхронно меняют индекс. Выбор зависит от договора получения данных, а не от одного предпочитаемого метода коллекции.
Избранное при таком обновлении не обязано исчезать. Оно хранит намерение читателя по стабильным идентификаторам. Для исчезнувшего курса индекс вернёт отсутствие, и интерфейс сможет показать понятный результат или предложить очистку. Если вместо идентификаторов хранить старые объекты карточек, выбор окажется привязан к прежней версии данных и станет сложнее согласовать.
У строкового ключа важен точный формат. javascript и JavaScript являются разными значениями для нашего индекса. Приведение регистра в одном месте чтения не заменяет договор всех производителей. Уже знакомая проверка модели задаёт допустимый формат прежде, чем индекс будет построен, поэтому зависимые операции используют одну идентичность.
Для наблюдения сохраните исходный индекс, затем создайте отдельный новый массив с копией одной изменённой записи и новый индекс. Сравните названия по одному ключу. Ожидается, что прежний индекс показывает прежний объект, а новый — новую запись. Это не ошибка Map, а следствие ссылок и времени создания. После упражнения верните четыре базовых курса, чтобы следующая последовательность порций оставалась предсказуемой.
Для ручной сверки создайте индекс, получите известный и неизвестный id, добавьте один идентификатор избранного дважды и затем удалите. Ожидается различие наличия и количества без дубликатов. Временный повтор id во входе должен вызвать наш отказ, а не молча заменить карточку. Следующий урок позволит получать части последовательности через итератор, не смешивая её порядок с индексом доступа.