В «Камне, ножницах, бумаге» мы хранили несколько чисел и строк. Теперь начинаем отдельный маленький проект: две карточки, каждую можно открыть и снова закрыть общим сбросом. У карточки сразу три связанных значения: имя, содержимое и признак открытия. Объект позволит держать их вместе. Главное — научиться создавать независимые карточки, чтобы действие над одной не меняло другую.

Открыть результат в отдельной вкладке

Подготовь файлы карточек

Нужны условия, функции, обработчики кнопок и массивы из предыдущего проекта. Его файлы сохраняем отдельно. Скачай архив ниже, скопируй содержимое 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:

БылоСтало
firstIdfirstCard.id
firstValuefirstCard.value
firstOpenfirstCard.open
secondIdsecondCard.id
secondValuesecondCard.value
secondOpensecondCard.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 на каждый элемент.