Объекты и модель учебного курса
Название курса и число уроков относятся к одной карточке. Хранить их в раздельных массивах означает поддерживать скрытое соответствие позиций. Если в одном списке удалить элемент, подпись другого курса получит чужое количество. Объект объединяет связанные свойства в одну запись с именованными полями.
В этом уроке введём окончательную схему данных catalog-lab. На неё будут опираться фильтрация, создание DOM и загрузка JSON. Не меняйте названия полей от урока к уроку: постоянный контракт позволяет изучать новый механизм, не перестраивая одновременно весь пример.
Свойства одной записи
Начнём с полного app.js, описывающего первый курс:
const course = {
id: 'javascript',
title: 'Современный JavaScript',
topic: 'frontend',
lessons: 20,
url: './courses/javascript.html',
};
console.log(course.title);
console.log(course.lessons + 1);
console.log(course['topic']);
Ожидаются название, число двадцать один и код frontend. В фигурных скобках каждая пара задаёт имя свойства и его значение. Запятая разделяет свойства, двоеточие связывает имя со значением. Это другая роль фигурных скобок, чем блок инструкций: контекст присваивания показывает, что создаётся объект.
Точка удобна, когда имя свойства известно в программе. Квадратные скобки позволяют получить свойство по вычисленному имени. Например, при const field = 'title' выражение course[field] прочитает название. Запись course.field будет искать буквальное свойство field, которого в нашей схеме нет.
id является машинным идентификатором. Название может измениться, позиция в массиве может поменяться, но javascript остаётся ссылкой на тот же курс. Нельзя использовать отображаемое название как единственный ключ: у разных материалов могут оказаться похожие названия, а редактор может исправить орфографию без создания нового курса.
Четыре записи каталога
Теперь полностью замените файл общими данными серии. URL здесь относятся к учебным страницам вашего проекта; соответствующие локальные заглушки входят в итоговый fixture. Они не означают, что четыре будущих курса уже опубликованы на ProfessorWeb.
const courses = [
{ id: 'javascript', title: 'Современный JavaScript', topic: 'frontend',
lessons: 20, url: './courses/javascript.html' },
{ id: 'html-css', title: 'HTML и CSS', topic: 'frontend',
lessons: 16, url: './courses/html-css.html' },
{ id: 'performance', title: 'Производительность сайта', topic: 'frontend',
lessons: 12, url: './courses/performance.html' },
{ id: 'markdown', title: 'Статический сайт из Markdown', topic: 'publishing',
lessons: 12, url: './courses/markdown.html' },
];
for (const course of courses) {
console.log(`${course.title}: уроков ${course.lessons}`);
}
Ожидаются четыре подписи. Цикл получает каждый объект целиком; название и количество читаются из одной записи. Теперь добавление курса требует добавить один объект, а не одновременно дописать несколько параллельных массивов. Это уменьшает количество мест, где может нарушиться соответствие.
Машинная тема publishing относится к публикации сайтов. В интерфейсе она получит понятную русскую подпись. Значение frontend относится к трём первым курсам. Общая сумма уроков равна шестидесяти; позднее вычислим её из записей, чтобы не хранить отдельное число, которое может устареть.
Отсутствующее свойство
Чтение courses[0].subtitle даст undefined, потому что такого свойства нет в нашей записи. JavaScript не заставляет все объекты иметь одинаковые поля. Это удобно для динамического языка, но приложение должно само определять ожидаемую схему, особенно если данные загружены извне.
Не подменяйте обязательное название пустой строкой лишь ради отсутствия ошибки. Если поле title потеряно, пустая карточка скрывает повреждение данных. Для необязательной аннотации запасная пустая строка может быть уместной; для обязательного поля нужен понятный отказ на границе загрузки.
Различайте отсутствие свойства и отсутствие самого объекта. Если courses[10] равно undefined, чтение .title у него вызовет ошибку. Проверка верхнего уровня и проверка отдельного поля решают разные задачи. Порядок будущей проверки данных начинается с массива, затем объекта и только после этого его свойств.
Объекты и способы обращения к свойствам объясняются в MDN о работе с объектами. Наш контракт является договорённостью проекта поверх возможностей языка.
Копирование и ответственность
Присваивание const selected = courses[0] не копирует карточку. Оба выражения указывают на один объект. Если выполнить selected.lessons = 21, количество изменится в исходном массиве. Это остаётся возможным при объявлении courses через const, потому что связь массива с именем не переназначается.
Для учебного отображения договоримся не менять свойства карточек. Отбор возвращает массив ссылок на существующие объекты, а форматирование читает их поля. Если появится редактор, создание новой версии записи станет отдельной операцией, которую можно ясно назвать и проверить. Незаметная правка внутри функции создания подписи нарушила бы эту границу.
Поверхностную копию объекта можно получить spread-записью { ...course }; подробный разбор будет в уроке о деструктуризации. Даже она не копирует глубоко вложенные объекты. Наша плоская схема состоит из строк и числа, поэтому этого ограничения для отдельных новых записей достаточно понимать без сложного механизма клонирования.
Для самостоятельного наблюдения найдите в листинге единственное место, где задаётся число уроков Markdown-курса. Измените его временно на тринадцать и предскажите подпись. Затем верните двенадцать, чтобы следующие результаты совпадали. Эта маленькая операция показывает преимущество объекта: связанная информация редактируется в одной записи.
Объект программы и JSON
Текущий листинг является JavaScript, а не файлом JSON. В нём допускаются незаключённые в кавычки имена свойств и строки в одинарных кавычках. Когда перенесём данные в JSON, синтаксис станет строже: имена и строковые значения будут заключены в двойные кавычки, комментарии и завершающие лишние запятые не войдут в формат. Не переименовывайте app.js в JSON и не ожидайте, что всё сохранится автоматически.
Кроме данных объект JavaScript способен хранить функции, но карточке такая возможность сейчас не нужна. Число уроков должно оставаться числом, а действие отображения принадлежит программе. Это облегчает будущую передачу данных: JSON описывает значения, не переносит браузеру функции поведения.
Наша схема плоская и намеренно небольшая. Если позднее появятся авторы, главы и связи между курсами, для них потребуется отдельное решение: вложить набор в каждую запись или использовать постоянные идентификаторы связанных сущностей. Не добавляйте пустые поля «на будущее» без обязанности, которую они выполняют. Каждое новое обязательное свойство означает изменение контрактов загрузки, проверки и отображения, а не только дополнительную строку литерала.
У объекта могут быть свойства со значением undefined. Это похоже на отсутствие свойства при обычном чтении, но не обязательно одинаково для перечисления или передачи данных. Для обязательных полей карточки такую неоднозначность не используем: каждое поле получает явное допустимое значение. Это делает результат проверки и отображения предсказуемым.
Свойство url не вычисляется из текущего названия автоматически. Такой выбор защищает ссылку от редакторской правки заголовка: название можно уточнить, сохранив страницу назначения. Постоянный идентификатор и адрес выполняют разные роли, даже когда в маленьком fixture их строки похожи. Не связывайте адрес с номером карточки, который меняется после сортировки.
При передаче объекта в функцию параметр получает доступ к той же записи. Функция способна изменить её свойства, если явно это делает. Поэтому отсутствие return нового объекта ещё не доказывает отсутствие изменения исходника. В наших функциях отбора и подписи просматривайте тело: оно только читает поля. Это проверка обязанности по коду, которая затем должна быть подтверждена ручным наблюдением поведения, когда запуск будет разрешён.
Теперь каталог имеет единую модель. Дальше будем получать из массива только подходящие курсы, преобразовывать их в подписи и считать сумму. Функции массивов выполнят эти действия без ручного изменения источника и без привязки к определённым индексам.