В «Камне, ножницах, бумаге» мы хранили несколько чисел и строк. Теперь начинаем отдельный маленький проект: две карточки, каждую можно открыть и снова закрыть общим сбросом. У карточки сразу три связанных значения: имя, содержимое и признак открытия. Объект позволит держать их вместе. Главное — научиться создавать независимые карточки, чтобы действие над одной не меняло другую.
Открыть результат в отдельной вкладке
Подготовь файлы карточек
Нужны условия, функции, обработчики кнопок и массивы из предыдущего проекта. Его файлы сохраняем отдельно. Скачай архив ниже, скопируй содержимое start в новую папку card-workshop и открой её index.html. В ней лежат index.html, style.css и game.js. Сервер, установка пакетов и аккаунт для этого примера не нужны.
Старт уже работает на знакомых переменных: открой первую карточку — вторая должна остаться закрытой. Сброс закроет обе. В game.js прочитай две функции действия и render. Данные хранятся в переменных; надписи кнопок только показывают их. disabled запрещает нажимать уже открытую карточку. В конце файла обработчики подключаются один раз, затем вызывается первое отображение.
В стартовом файле элементы найдены через document.querySelector("#card-one"). Это ещё один способ поиска в DOM: он принимает CSS-селектор и возвращает первый подходящий элемент, а если его нет — null. Знак # означает поиск по HTML-id, поэтому здесь он нужен. Знакомый getElementById("card-one") принимает само имя без #. В этом проекте id уникальны; после поиска работа с textContent, disabled и обработчиками остаётся прежней.
Собираем свойства в объект
В начале game.js сейчас шесть объявлений — по три на карточку. Замени только этот первый блок двумя объектами:
const firstCard = { id: "sun-1", value: "Солнце", open: false };
const secondCard = { id: "moon-1", value: "Луна", open: false };
Фигурные скобки здесь создают объект. Внутри записаны свойства в виде имя: значение, они разделены запятыми. id — устойчивое имя карточки, value — что она скрывает, open — признак того, открыта ли карточка. false означает «закрыта», true — «открыта». Имя свойства id в данных само по себе не создаёт HTML-атрибут: кнопки мы по-прежнему находим явно.
Дальше в этом же файле замени обращения к прежним переменным. Используй поиск редактора; замена должна охватить и действия, и сброс, и render:
| Было | Стало |
|---|---|
| firstId | firstCard.id |
| firstValue | firstCard.value |
| firstOpen | firstCard.open |
| secondId | secondCard.id |
| secondValue | secondCard.value |
| secondOpen | secondCard.open |
Например, первое действие теперь такое:
function handleFirstCard() {
firstCard.open = true;
render();
}
Точка читает или изменяет свойство конкретного объекта. Объявление const firstCard не запрещает изменение свойств: оно запрещает присвоить самой переменной другой объект. Здесь переменная остаётся связана с той же карточкой. Сохрани файл, обнови страницу и проверь открытие в обоих порядках и сброс. Поведение не изменилось — изменилось устройство данных. Это версия step-01.
Точка и квадратные скобки
Открой Console этой версии. Перед вводом попробуй предсказать вывод:
console.log(firstCard.value);
console.log(firstCard["value"]);
const propertyName = "value";
console.log(firstCard[propertyName]);
Все три строки выводят «Солнце». Скобки вычисляют имя свойства из выражения внутри. Поэтому firstCard[propertyName] полезно, когда имя хранится в переменной. Запись firstCard.propertyName ищет буквальное свойство с названием propertyName; его нет, результат — undefined. В самой игре остаёмся с точкой: нужные свойства известны заранее. Если Console жалуется на повторное объявление const, обнови страницу и повтори опыт один раз.
Почему присваивание не создаёт вторую карточку
Следующие два опыта выполни в Console. Они используют отдельные имена и не меняют карточки игры. Сначала числовое, то есть примитивное, значение:
let originalScore = 3;
let copiedScore = originalScore;
copiedScore = 9;
console.log(originalScore, copiedScore); // 3 9
Мы присвоили второй переменной новое число; первая осталась равна 3. Теперь объект:
const originalCard = { id: "test-1", value: "Ключ", open: false };
const aliasCard = originalCard;
aliasCard.open = true;
console.log(originalCard.open); // true
console.log(aliasCard === originalCard); // true
Обе переменные ссылаются на один объект. aliasCard — ещё одна ссылка на тот же объект. Поэтому через него изменилось состояние, видимое и через originalCard. Для двух независимых карточек нужны два объекта. Одинаковые значения свойств ещё не означают один объект:
const separateCard = { id: "test-2", value: "Ключ", open: false };
console.log(originalCard === separateCard); // false
console.log(separateCard.open); // false
Начальные значения карточек известны, поэтому при сбросе можно создавать объекты заново. Это также не даст изменённой партии испортить образец для сброса.
Создаём новый набор при каждом сбросе
В game.js версии step-01 замени два объявления firstCard и secondCard в самом начале на функцию и переменную:
function createInitialCards() {
return [
{ id: "sun-1", value: "Солнце", open: false },
{ id: "moon-1", value: "Луна", open: false }
];
}
let cards = createInitialCards();
Такая функция часто называется фабрикой: при каждом вызове она создаёт и возвращает новые данные. Скобки [] создают массив, а две пары {} внутри — два отдельных объекта. Первую карточку читаем через cards[0], вторую через cards[1]. Например, cards[0].open сначала выбирает элемент массива, затем его свойство.
Во всём оставшемся файле замени firstCard на cards[0], а secondCard на cards[1]. После этого замени функцию сброса целиком:
function handleReset() {
cards = createInitialCards();
render();
}
Теперь сброс получает новый массив и новые объекты, а не пытается вернуть каждое поле старых объектов вручную. Поэтому cards объявлена через let: при сбросе переменная получает другой массив. Старые переменные-ссылки, если они остались у тебя в Console, продолжают указывать на прежние объекты — не на новую партию.
Кнопки остаются прежними. Их обработчики при нажатии читают текущую переменную cards. Не переноси addEventListener внутрь render или сброса. Две кнопки пока выводятся вручную; автоматическое построение большого поля будет отдельной задачей.
Обнови страницу. Открой только sun-1, нажми сброс и затем открой только moon-1. Закрытая карточка не должна меняться. Повтори сброс несколько раз. Это финальная версия урока.
Полный game.js для сверки после этапа
function createInitialCards() {
return [
{ id: "sun-1", value: "Солнце", open: false },
{ id: "moon-1", value: "Луна", open: false }
];
}
let cards = createInitialCards();
const firstButton = document.querySelector("#card-one");
const secondButton = document.querySelector("#card-two");
const resetButton = document.querySelector("#reset-button");
const statusElement = document.querySelector("#status");
function handleFirstCard() {
cards[0].open = true;
render();
}
function handleSecondCard() {
cards[1].open = true;
render();
}
function handleReset() {
cards = createInitialCards();
render();
}
function render() {
if (cards[0].open) {
firstButton.textContent = cards[0].id + ": " + cards[0].value;
} else {
firstButton.textContent = "Открыть " + cards[0].id;
}
firstButton.disabled = cards[0].open;
if (cards[1].open) {
secondButton.textContent = cards[1].id + ": " + cards[1].value;
} else {
secondButton.textContent = "Открыть " + cards[1].id;
}
secondButton.disabled = cards[1].open;
if (cards[0].open && cards[1].open) {
statusElement.textContent = "Обе карточки открыты. Сбрось набор и проверь ещё раз.";
} else {
statusElement.textContent = "Открой обе карточки.";
}
}
firstButton.addEventListener("click", handleFirstCard);
secondButton.addEventListener("click", handleSecondCard);
resetButton.addEventListener("click", handleReset);
render();Проверяем независимость данных
В Console финальной версии выполни опыт. Это проверка данных отдельно от интерфейса, поэтому кнопки не должны поменяться:
const firstSet = createInitialCards();
const secondSet = createInitialCards();
firstSet[0].open = true;
console.log(firstSet[1].open); // false: соседняя карточка независима
console.log(secondSet[0].open); // false: другой набор тоже независим
console.log(firstSet[0] === firstSet[1]); // false
console.log(firstSet[0] === secondSet[0]); // false
Ещё один опыт уже с игрой: введи cards[0].value = "Изменено";, затем render(); и открой первую карточку. Она покажет изменённое значение. После сброса и нового открытия должно снова появиться «Солнце»: восстановилось и содержимое, и признак открытия.
Ошибка, которую важно узнавать: const same = { id: "one", value: "Ключ", open: false }; const wrong = [same, same];. Здесь два места массива содержат одну и ту же ссылку. Изменение wrong[0].open видно и через wrong[1].open. Копирование самого массива тоже не создаёт новые вложенные объекты автоматически. Наше решение — каждый раз выполнять отдельные выражения {...} в фабрике; многоточие здесь лишь обозначает поля, это не код для вставки.
Самостоятельно: свой набор из трёх карточек
Возьми свою финальную версию и создай три карточки. У каждой уникальный id; у первых двух одинаковое value, у третьей другое. Это заготовка для поиска пар, пока без сравнения и задержек. Добавь третью кнопку, отдельный обработчик и ручной вывод. Надпись об окончании должна появляться, только когда открыты все три. Сброс закрывает весь набор и возвращает исходные значения.
Перед изменением кода запиши, как должна вести себя игра: открываю первую из одинаковых — вторая закрыта; открываю третью — вторая всё ещё закрыта; открываю вторую — набор завершён; сбрасываю — все закрыты. Одинаковая картинка не должна означать один общий объект. Реализуй свой набор без копирования готового решения. Имена функций и синтаксис можно подсмотреть выше.
Подсказка 1: что нужно добавить
Нужны новый объект внутри фабрики, кнопка внутри #card-field, ссылка на кнопку в JS, функция действия, отображение и одно подключение обработчика. Для третьего объекта индекс равен 2.
Подсказка 2: третья карточка и конец игры
Третья кнопка может иметь HTML id card-three. Её действие меняет cards[2].open. Условие окончания должно проверить три свойства open через &&. В createInitialCards запиши третий отдельный объект, не повторяй ссылку на первый.
Решение после собственной попытки
Открыть пример из трёх карточек. Исходники лежат в solutions/three-cards. У двух ключей разные id, а одинаковое содержимое. Сравни только тот участок, который не получился, затем проверь другой собственный набор.
Проверь, что карточки не влияют друг на друга
- Ты читаешь свойство точкой и скобками, объясняешь разницу между именем свойства и переменной с этим именем.
- Предсказываешь результат двух опытов: новое число и два имени одного объекта.
- Открытие одной карточки не открывает соседнюю даже при одинаковом содержимом.
- Каждый вызов фабрики возвращает независимые объекты; сброс возвращает значения и закрывает карточки.
- Свой набор из трёх карточек работает и корректно сообщает об окончании.
Закрепи принцип без интерфейса: напиши функцию, которая возвращает новый объект персонажа с именем и количеством энергии. Создай двух персонажей, уменьши энергию первого и проверь, что второй не изменился. Готовый код карточек целиком для этого не нужен.
Файлы и справка
В архиве start — знакомые отдельные переменные, step-01 — два объекта, final — массив и фабрика нового состояния; solutions — решение самостоятельного задания. Открывай index.html нужной папки, не смешивай её файлы с другой версией.
Справочно: свойства и сравнение объектов на MDN, примитивные значения, что ограничивает const. Читать справочники целиком для перехода дальше не требуется.
Карточки сохрани: позже вернёмся к ним и построим поле по массиву, без отдельной кнопки в HTML на каждый элемент.