Вернёмся к карточкам из урока об объектах. До этого у каждой была своя кнопка и отдельная функция. С двумя карточками это удобно, но набор из восьми заставил бы повторять HTML и JS. Построим поле по массиву: сколько объектов в данных, столько кнопок появится на странице.
Возвращаемся к знакомым файлам
Нужны массивы, объекты, функции и обработчики. После игры на реакцию возвращаемся к карточкам: start здесь точно совпадает с final урока про независимые карточки. Скопируй эти три файла в новую рабочую папку. Свой набор из прошлого задания сохрани отдельно: позже перенесёшь его данные.
Открой index.html, проверь две карточки и сброс. Наша фабрика createInitialCards уже возвращает новый массив независимых объектов. Оставим её. Поменяем способ создания кнопок и отображения. Файлы запускаются открытием HTML; HTTP-сервер для этой версии пока не нужен.
Один цикл вместо одинаковых команд
Цикл повторяет блок кода, пока выполняется условие. В Console стартовой версии выполни:
for (let index = 0; index < cards.length; index = index + 1) {
console.log(index, cards[index].id);
}
Первая часть выполняется один раз: создаётся счётчик index со значением 0. Перед каждой итерацией, то есть повторением блока, проверяется index < cards.length. После блока индекс увеличивается на 1. Для двух карточек выводятся 0 и 1; при 2 условие уже ложно. Знак <= здесь был бы ошибкой: элемента с индексом length нет.
Это перебор конечного набора данных, а не анимация и не таймер. Браузер выполняет итерации подряд. Не удаляй увеличение счётчика в таком цикле: если условие навсегда остаётся истинным, вкладка перестаёт отвечать.
Оставляем место для поля
В index.html убери две кнопки только из #card-field. Сам контейнер и кнопку сброса оставь:
<div id="card-field" class="card-field"></div>
В game.js удали объявления firstButton/secondButton, функции handleFirstCard/handleSecondCard и две строки, которые подключали их к кнопкам. Старую render и handleReset сейчас заменим. Сохрани createInitialCards, let cards = createInitialCards();, resetButton и statusElement. Рядом с объявлениями элементов добавь:
const cardField = document.querySelector("#card-field");
Заодно подготовим поле к большему набору на узком экране. В конец style.css добавь знакомое из урока Flexbox правило для маленькой ширины:
@media (max-width: 420px) {
.card-field button {
flex-basis: 90px;
min-height: 96px;
padding: 10px;
}
}
Меньшее значение flex-basis позволяет двум карточкам помещаться рядом вместо длинной колонки. Высота и отступы оставляют достаточно места для нажатия. Проверь две карточки в узком окне; при переходе к шести это же правило сохранит компактное поле. Стартовая версия ещё содержит прежнее оформление, а step-01 и финал — новое.
Создаём элементы из данных
Ниже объявлений элементов добавь buildField. Она строит кнопки один раз для текущего набора:
function buildField() {
cardField.textContent = "";
for (let index = 0; index < cards.length; index = index + 1) {
const card = cards[index];
const button = document.createElement("button");
button.type = "button";
button.id = "card-" + card.id;
button.dataset.cardId = card.id;
button.addEventListener("click", function () {
openCard(card);
});
cardField.appendChild(button);
}
}
document.createElement("button") создаёт элемент в памяти; appendChild добавляет его в контейнер. До добавления мы задаём тип, идентификатор и обработчик. cardField.textContent = "" очищает прежние дочерние элементы. Поэтому сброс не допишет второй набор под первым.
Свойство dataset.cardId записывает строковый атрибут data-card-id. Его видно в инспекторе Elements; он помогает проверить связь кнопки с данными. HTML-id кнопки получаем как "card-" + card.id, чтобы затем найти её. У каждого объекта должен быть уникальный id: значение может повторяться, идентификатор — нет. Позиция в массиве может измениться, а имя карточки остаётся тем же.
Обработчик здесь — функция без имени, созданная внутри итерации и переданная в addEventListener. Она не выполняется при построении. При будущем клике она всё ещё имеет доступ к переменной card своей итерации: это называется замыканием. Каждая итерация создаёт свою const card, поэтому кнопка работает со своим объектом. Мы не сохраняем в обработчик общий меняющийся индекс.
Сразу после buildField добавь действие:
function openCard(card) {
if (card.open) {
return;
}
card.open = true;
render();
}
Параметр card связан с тем же объектом, который лежит в массиве. Изменение свойства обновляет данные этой карточки. Повторное действие на уже открытой карточке прекращается через return.
Перебираем объекты и обновляем надписи
В старую render вместо ручного вывода двух кнопок поставь:
function render() {
let openCount = 0;
for (const card of cards) {
const button = document.getElementById("card-" + card.id);
if (card.open) {
button.textContent = card.id + ": " + card.value;
openCount = openCount + 1;
} else {
button.textContent = "Открыть " + card.id;
}
button.disabled = card.open;
}
statusElement.textContent = "Открыто: " + openCount + " из " + cards.length + ".";
if (openCount === cards.length) {
statusElement.textContent = "Все карточки открыты. Сбрось набор и попробуй ещё раз.";
}
}
Запись for (const card of cards) по очереди даёт каждый элемент массива. Это for...of: индекс не нужен, поэтому запись короче. Слово of важно; for...in — другая конструкция, здесь она не используется.
getElementById находит элемент по его HTML-id, без знака #. Кнопки уже созданы, теперь мы меняем только текст и доступность. Не создаём новый элемент после каждого клика. openCount — временный счётчик открытых карточек: при каждом отображении начинаем с нуля и вычисляем его по данным.
Создаём новое поле при сбросе
На месте старой функции сброса добавь resetGame. В самом низу файла удали старые подключения и первый вызов render, затем оставь ровно три строки запуска из этого фрагмента:
function resetGame() {
cards = createInitialCards();
buildField();
render();
}
resetButton.addEventListener("click", resetGame);
buildField();
render();
В этой версии порядок важен: сначала данные, затем элементы, затем их надписи. Обработчик кнопки сброса подключается один раз. При создании новых карточек прежние кнопки удаляются вместе со своими обработчиками; к каждой новой подключается один. render не подключает обработчики.
Сохрани и обнови страницу. Две карточки должны работать как раньше. После пяти сбросов на поле всё ещё две кнопки. Это step-01. Если экран пуст, сначала смотри Console: частая причина — оставшийся вызов удалённой функции или render, запущенная до buildField.
Сверить полный game.js промежуточной версии
function createInitialCards() {
return [
{ id: "sun-1", value: "Солнце", open: false },
{ id: "moon-1", value: "Луна", open: false }
];
}
let cards = createInitialCards();
const cardField = document.querySelector("#card-field");
const resetButton = document.querySelector("#reset-button");
const statusElement = document.querySelector("#status");
function buildField() {
cardField.textContent = "";
for (let index = 0; index < cards.length; index = index + 1) {
const card = cards[index];
const button = document.createElement("button");
button.type = "button";
button.id = "card-" + card.id;
button.dataset.cardId = card.id;
button.addEventListener("click", function () {
openCard(card);
});
cardField.appendChild(button);
}
}
function openCard(card) {
if (card.open) {
return;
}
card.open = true;
render();
}
function render() {
let openCount = 0;
for (const card of cards) {
const button = document.getElementById("card-" + card.id);
if (card.open) {
button.textContent = card.id + ": " + card.value;
openCount = openCount + 1;
} else {
button.textContent = "Открыть " + card.id;
}
button.disabled = card.open;
}
statusElement.textContent = "Открыто: " + openCount + " из " + cards.length + ".";
if (openCount === cards.length) {
statusElement.textContent = "Все карточки открыты. Сбрось набор и попробуй ещё раз.";
}
}
function resetGame() {
cards = createInitialCards();
buildField();
render();
}
resetButton.addEventListener("click", resetGame);
buildField();
render();Добавляем карточки в массив
В game.js замени фабрику таким набором из шести объектов:
function createInitialCards() {
return [
{ id: "sun-1", value: "Солнце", open: false },
{ id: "sun-2", value: "Солнце", open: false },
{ id: "moon-1", value: "Луна", open: false },
{ id: "moon-2", value: "Луна", open: false },
{ id: "star-1", value: "Звезда", open: false },
{ id: "star-2", value: "Звезда", open: false }
];
}
Открой страницу: появились шесть кнопок без шести новых HTML-строк. Две карточки «Солнце» — разные объекты, потому что это разные выражения { id: ..., value: ..., open: ... }. Их значения совпадают, но идентификаторы разные. Пока мы открываем все карточки; правила поиска пар появятся в отдельной игре.
В buildField индекс больше не используется нигде, кроме чтения объекта. Можно заменить начало цикла из двух строк на for (const card of cards) {. Весь остальной блок остаётся прежним. Так мы использовали for, когда нужен счётчик, и for...of, когда нужен сам объект. Это final.
Самостоятельно: добавь карточки и поменяй их порядок
Собери набор из восьми карточек с четырьмя повторяющимися значениями и восемью разными id. Меняй только фабрику: ни HTML, ни обработчики, ни render расширять не нужно. Поменяй местами два объекта в массиве. Карточки должны появиться в новом порядке, сохранив свои имена и значения. Затем временно оставь один объект: его открытие должно завершать набор.
Верни восемь объектов и проверь: открытие одного из одинаковых значений не открывает другое; после нескольких сбросов карточек ровно восемь, счёт начинается с нуля, один клик открывает одну карточку.
Подсказка 1: что управляет количеством
Построение и отображение перебирают cards. Ищи изменение в createInitialCards, а не в разметке. После правки обнови страницу либо нажми сброс.
Подсказка 2: одна пара — два объекта
Добавь два отдельных объекта с одинаковым value, но разными id. Не вставляй одну ссылку на объект дважды. Проверь запятые между объектами.
Решение после попытки
Пример набора из восьми карточек находится в solutions/eight-cards. Между ним и финалом отличается только фабрика. Затем попробуй составить свой набор по тому же принципу.
Проверяем результат
- Ты объясняешь начальный индекс, условие продолжения и увеличение в
for. - Различаешь перебор массива и действие с задержкой.
- Знаешь, где создаётся кнопка, где добавляется в документ и когда выполняется её обработчик.
- Ты меняешь число и порядок карточек через массив. Карточки с одинаковым содержимым открываются независимо друг от друга.
- После повторного сброса нет дубликатов кнопок и лишних действий на один клик.
Карточка — данные, кнопка — её отображение. Сохрани эту версию: на том же поле можно строить правила поиска пар.
Материалы урока
Справочно: циклы, создание элементов и замыкания на MDN. Сначала проверь свой работающий пример, затем обращайся к нужной справке.