ФронтендJavaScriptHTML / CSS

Викторина на JavaScript: вопросы, варианты, счёт и новый раунд

Делаем викторину на JavaScript из массива вопросов: варианты ответов, подсветка результата, счёт, прогресс и кнопка повторного прохождения.

Кодик

Автор

5 мин чтения

У викторины всего три состояния: текущий вопрос, число правильных ответов и выбран ли уже вариант. Если хранить их явно, кнопки не начислят два балла, следующий вопрос появится вовремя, а повторный раунд начнётся с нуля.

Новички часто создают отдельную HTML-кнопку для каждого вопроса. Тогда изменение текста требует править разметку, а логика расползается по обработчикам. Мы положим данные в массив и будем рисовать один универсальный экран.

1Храним

Вопрос, варианты и индекс ответа лежат в одном объекте.

2Рисуем

Одна renderQuestion обновляет заголовок, прогресс и кнопки.

3Защищаем

Флаг answered блокирует повторный балл до перехода дальше.

Как представить вопросы в данных

У каждого вопроса есть text, answers и correct. Индекс текущего вопроса начинается с нуля, score тоже. После клика кнопки получают disabled, правильный вариант отмечается всегда, а выбранный неправильный получает отдельный класс.

ПолеТипПример
textstringЧто делает const?
answersstring[]Массив из трёх вариантов
correctnumberИндекс правильного варианта
currentnumberКакой объект сейчас показывать

После этого шага у каждой части есть одна роль. Интерфейс собирает действие, логика меняет состояние, а вывод показывает признак успеха. Это проще отлаживать, чем один большой обработчик.

Жизнь одного вопроса
answered отделяет выбор от перехода. После последнего вопроса появляется итог

Сначала соберите минимальный сценарий. Не добавляйте оформление, хранение и дополнительные режимы одновременно. Добейтесь одного правильного прохода от входных данных до результата, а затем проверяйте каждое улучшение отдельно.

Пишем рендер и проверку ответа

Используйте делегирование событий: один обработчик стоит на контейнере ответов. Индекс ответа хранится в data-index. После выбора мы сначала ставим answered=true, а уже затем считаем и красим, поэтому быстрый двойной клик не изменит счёт дважды.

const questions = [
  {text:'Как объявить неизменяемую ссылку?',answers:['let','const','var'],correct:1},
  {text:'Что возвращает Array.isArray([])?',answers:['true','false','undefined'],correct:0},
  {text:'Какое событие вызывается кнопкой?',answers:['click','submitter','press'],correct:0},
];
let current=0, score=0, answered=false;
const title=document.querySelector('#question');
const answers=document.querySelector('#answers');
const progress=document.querySelector('#progress');

function renderQuestion(){
  answered=false;
  const item=questions[current];
  progress.textContent='Вопрос ' + (current+1) + ' из ' + questions.length;
  title.textContent=item.text;
  answers.innerHTML=item.answers.map((text,index)=>
    '<button data-index="' + index + '">' + text + '</button>').join('');
}

answers.addEventListener('click',event=>{
  const button=event.target.closest('button');
  if(!button || answered) return;
  answered=true;
  const chosen=Number(button.dataset.index);
  if(chosen===questions[current].correct) score++;
  [...answers.children].forEach((item,index)=>{
    item.disabled=true;
    if(index===questions[current].correct) item.classList.add('correct');
    else if(index===chosen) item.classList.add('wrong');
  });
});

document.querySelector('#next').addEventListener('click',()=>{
  if(!answered) return;
  current++;
  if(current<questions.length) renderQuestion();
  else document.querySelector('#quiz').textContent='Результат: ' + score + '/' + questions.length;
});
renderQuestion();
Что должно работать после первого запуска

Вопросы отделены от разметки и легко дополняются. Один контейнер обрабатывает все варианты. После ответа кнопки блокируются и показывают правильный. Финальный экран считает результат из длины массива.

Где хранить вопросы
Данные не должны копировать интерфейс. Сравнение показывает, какой вариант легче поддерживать и проверять.

Как не начислить один балл дважды

innerHTML здесь получает строки из вашего статического массива. Если вопросы приходят от пользователей или внешнего API, не вставляйте их как HTML: создавайте кнопки через createElement и назначайте textContent. Это не даёт данным превратиться в разметку или скрипт.

СостояниеРазрешённое действиеСледующий шаг
answered=falseВыбрать вариантПроверить и заблокировать
answered=trueНажать Далееcurrent + 1
current < lengthПоказать вопросСбросить answered
current = lengthПоказать итогПредложить повтор

Сохраняйте в одном месте то, что может измениться. Если одно и то же значение скопировано в три функции, после первой правки они начнут расходиться. Один объект состояния и одна функция отрисовки снимают эту проблему.

Смотрите на состояние, а не только на красивый экран. Перезагрузка, повторный клик, пустой ввод и медленный ответ часто открывают ошибки, которые не видны в идеальном демонстрационном проходе.

Практика: проходим все ветки

Добавьте вопрос, где правильный ответ стоит последним, затем первый и средний. Дважды быстро нажмите один вариант, попробуйте кнопку «Далее» до выбора и пройдите тест до конца без правильных ответов. Итог должен оставаться предсказуемым. После этого пройдите все вопросы правильно и убедитесь, что знаменатель результата всегда берётся из длины массива.

Проверка своими руками
  1. Добавьте четвёртый вопрос.
  2. Проверьте правильный ответ с индексом 0.
  3. Нажмите Далее до ответа.
  4. Сделайте двойной клик по варианту.
  5. Пройдите все вопросы неправильно.
  6. Добавьте кнопку «Начать заново», обнуляющую три состояния.
Готово, если выполняются все пункты

Один ответ даёт максимум один балл. Правильный вариант виден даже после ошибки. Далее не работает до выбора. Новый раунд обнуляет current, score и answered.

Четыре проверки
Игра готова после крайних сценариев. Пройдите все четыре сценария перед публикацией проекта.

Доводим викторину до финального экрана

После базовой версии можно перемешать вопросы, добавить пояснение и сохранять лучший результат. Не добавляйте всё сразу. Сначала вынесите resetQuiz и проверьте, что новый раунд полностью независим от старого.

Вопросы зашиты в HTML

Любое изменение требует копировать разметку.

Нет answered

Двойной клик меняет счёт дважды.

Далее работает сразу

Можно пропускать состояние без явного решения.

Непроверенный innerHTML

Внешние данные могут стать разметкой.

Сверьтесь с первичным источником. Поведение используемых функций и ограничения примера проверяйте по документацией MDN по closest. Документация особенно важна, когда меняются версии, политика платформы или формат ответа API.

Что получится в итоге

Готовый результат проекта: Викторина на JavaScript: вопросы, варианты, счёт и новый раунд
Финальная викторина показывает прогресс, крупные варианты, обратную связь после клика и аккуратный итоговый счёт.

Короткие ответы
Зачем хранить вопросы в массиве?

Данные можно менять без копирования интерфейса и обработчиков.

Что защищает от двойного балла?

Флаг answered и disabled у кнопок.

Когда innerHTML опасен?

Когда внутрь попадают непроверенные внешние или пользовательские данные.

Как понять, что проект готов?

Он повторяет целевой сценарий несколько раз подряд, понятно реагирует на ошибочный ввод и не теряет состояние при обычных действиях.

Можно ли копировать код целиком?

Можно как стартовую точку, но сначала замените значения из примера, затем запустите проект по шагам. Если вы не можете объяснить одну строку, её лучше разобрать до следующего шага.

Сделайте один движок для десятков вопросов

Массивы, объекты и события разбираются в курсе JavaScript. Если обработчик не находит кнопку, проверьте ошибку null.

Состояние между запусками можно сохранить по примеру Todo-листа, а таймер раунда добавить по статье об обратном отсчёте. В Кодике удобно тестировать каждый вопрос отдельно.