Доброго времени суток, друзья! Предлагаю Вашему вниманию небольшой интерактив — своего рода викторину по JavaScript, на данный момент состоящую из 50 вопросов.

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

Предисловие


Данная часть основана на этом репозитории. Его автор, Lydia Hallie, позиционирует свой проект как список продвинутых вопросов и, действительно, среди них есть такие, которые, как мне кажется, даже опытному JavaScript-разработчику покажутся непростыми. Однако среди этих вопросов есть и такие, для ответа на которые достаточно владеть базовыми знаниями. В репозитории имеется русский перевод, но, мягко говоря, он оставляет желать лучшего, поэтому большую часть ответов (объяснений) пришлось переводить заново.

Следует отметить, что приводимые пояснения (ответы) не всегда в полной мере раскрывают суть проблемы. Это объясняется формой проекта — он представляет собой чеклист, а не учебник. Ответы, скорее, являются подсказкой для дальнейших поисков на MDN или Javascript.ru. Впрочем, многие из объяснений содержат исчерпывающие ответы.

Несмотря на то, что код неоднократно протестирован, никто не застрахован от ошибок, разумеется, кроме тех, кто ничего не делает. Поэтому при обнаружении ошибок, опечаток, неточностей, некорректности формулировок и т.п., а также при желании улучшить перевод, прошу писать в личку, буду признателен (активность на GitHub также приветствуется).

Собственно, это все, что я хотел сказать в качестве предисловия.

Правила


Правила простые: 50 вопросов, 3-4 варианта ответа, рейтинг: количество правильных и неправильных ответов, прогресс: номер и количество вопросов.

По результатам определяется процент правильных ответов и делается вывод об уровне владения JavaScript: больше 80% — отлично, больше 50% — неплохо, меньше 50%… ну, Вы понимаете.

К каждому вопросу прилагается пояснение. При неправильном ответе данное пояснение раскрывается.

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

Но довольно слов, пора переходить к делу.

Викторина



Код проекта находится здесь.

Делимся результатами в комментариях.

Механика


Несколько слов о том, как реализована викторина для тех, кому интересно.

Разметка выглядит так:

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>200+ вопросов по JavaScript</title>
    <!-- шрифт -->
    <link href="https://fonts.googleapis.com/css2?family=Ubuntu&display=swap" rel="stylesheet">
    <!-- стили -->
    <link rel="stylesheet" href="style.css">
    <!-- основной скрипт с типом "модуль" -->
    <script type="module" src="script.js"></script>
</head>
<body></body>

Добавляем минимальные стили:

CSS:
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    font-family: Ubuntu, sans-serif;
    font-size: 1em;
    text-align: center;
    letter-spacing: 1.05px;
    line-height: 1.5em;
    color: #111;
    user-select: none;
}

@media (max-width: 512px) {
    * {
        font-size: .95em;
    }
}

html {
    position: relative;
}

body {
    padding: 1em;
    min-height: 100vh;
    background: radial-gradient(circle, skyblue, steelblue);
    display: flex;
    flex-direction: column;
    justify-content: start;
    align-items: center;
}

h1 {
    margin: .5em;
    font-size: 1.05em;
}

output {
    margin: .5em;
    display: block;
}

.score {
    font-size: 1.25em;
}

form {
    text-align: left;
}

form p {
    text-align: left;
    white-space: pre;
}

form button {
    position: relative;
    left: 50%;
    transform: translateX(-50%);
}

button {
    margin: 2em 0;
    padding: .4em .8em;
    outline: none;
    border: none;
    background: linear-gradient(lightgreen, darkgreen);
    border-radius: 6px;
    box-shadow: 0 1px 2px rgba(0, 0, 0, .4);
    font-size: .95em;
    cursor: pointer;
    transition: .2s;
}

button:hover {
    color: #eee;
}

label {
    cursor: pointer;
}

input {
    margin: 0 10px 0 2em;
    cursor: pointer;
}

details {
    font-size: .95em;
    position: absolute;
    bottom: 0;
    left: 50%;
    transform: translateX(-50%);
    width: 90%;
    background: #eee;
    border-radius: 4px;
    cursor: pointer;
}

details h3 {
    margin: .5em;
}

details p {
    margin: .5em 1.5em;
    text-align: justify;
    text-indent: 1.5em;
}

.right {
    color: green;
}

.wrong {
    color: red;
}


Исходники (assets) представляют собой массив объектов, где каждый объект имеет свойства question (вопрос), answers (ответы), rightAnswer (правильный ответ) и explanation (объяснение):

[
{
    question: `
        function sayHi() {
            console.log(name);
            console.log(age);
            var name = "Lydia";
            let age = 21;
        }

        sayHi();
    `,
    answers: `
        A: Lydia и undefined
        B: Lydia и ReferenceError
        C: ReferenceError и 21
        D: undefined и ReferenceError
    `,
    rightAnswer: `D`,
    explanation: `
        Внутри функции мы сначала определяем переменную name с помощью ключевого слова var. Это означает, что name поднимется в начало функции. Name будет иметь значение undefined до тех пор, пока выполнение кода не дойдет до строки, где ей присваивается значение Lydia. Мы не определили значение name, когда пытаемся вывести ее в консоль, поэтому будет выведено undefined. Переменные, определенные с помощью let (и const), также поднимаются, но в отличие от var, не инициализируются. Доступ к ним до инициализации невозможен. Это называется "временной мертвой зоной". Когда мы пытаемся обратиться к переменным до их определения, JavaScript выбрасывает исключение ReferenceError.
    `
},
...
]

Основной скрипт:

JavaScript
// импортируем массив объектов - исходники
import assets from './assets.js'

// IIFE
;((D, B) => {
    // заголовок - вопрос
    const title = D.createElement('h1')
    B.append(title)

    // рейтинг: количество правильных и неправильных ответов
    const score = D.createElement('output')
    score.className = 'score'
    B.append(score)

    // прогресс: порядковый номер вопроса
    const progress = D.createElement('output')
    progress.className = 'progress'
    B.append(progress)

    // контейнер для вопроса, вариантов ответа и кнопки для отправки формы
    const div = D.createElement('div')
    B.append(div)

    // получаем значения правильных и неправильных ответов из локального хранилища
    // или присваиваем переменным 0
    let rightAnswers = +localStorage.getItem('rightAnswers') || 0
    let wrongAnswers = +localStorage.getItem('wrongAnswers') || 0

    // получаем значение счетчика из локального хранилища
    // или присваиваем ему 0
    let i = +localStorage.getItem('i') || 0

    // рендерим вопрос
    showQuestion()

    // обновляем рейтинг и прогресс
    updateScoreAndProgress()

    function showQuestion() {
        // если значение счетчика равняется количеству вопросов
        // значит, игра окончена,
        // показываем результат
        if (i === assets.length) {
            return showResult()
        }

        // заголовок-вопрос зависит от значения счетчика - номера вопроса
        const titleText = {
            4: `Что не является валидным?`,
            9: `Что произойдет?`,
            12: `Назовите три фазы распространения событий`,
            13: `Все ли объекты имеют прототипы?`,
            14: `Каким будет результат?`,
            20: `Чему равно sum?`,
            21: `Как долго будет доступен cool_secret?`,
            23: `Каким будет результат?`,
            25: `Глобальный контекст исполнения создает две вещи: глобальный объект и this`,
            27: `Каким будет результат?`,
            29: `Каким будет результат?`,
            30: `Что будет в event.target после нажатия на кнопку?`,
            33: `Каким будет результат?`,
            34: `Какие из значений являются "ложными"?`,
            38: `Все в JavaScript это`,
            39: `Каким будет результат?`,
            40: `Каким будет результат?`,
            41: `Что возвращает setInterval?`,
            42: `Каким будет результат?`,
            48: `Каково значение num?`,
            49: `Каким будет результат?`
        }
        title.textContent = titleText[i] || `Что будет выведено в консоль?`

        // поскольку каждый элемент массива - это объект,
        // мы можем его деструктурировать, получив вопрос, правильный ответ и объяснение
        const {
            question,
            rightAnswer,
            explanation
        } = assets[i]

        // поскольку варианты ответа - это input type="radio",
        // строку необходимо преобразовать в массив (критерием является перенос строки - \n)
        // первый и последний элементы - пустые строки,
        // избавляемся от них с помощью slice(1, -1),
        // также удаляем пробелы
        const answers = assets[i].answers
            .split('\n')
            .slice(1, -1)
            .map(i => i.trim())

        // HTML-шаблон
        const template = `
        <form action="#">
            <p><em>Вопрос:</em><br> ${question}</p>

            <p><em>Варианты ответов:</em></p><br>
            ${answers.reduce((html, item) => html += `<label><input type="radio" name="answer" value="${item}">${item}</label><br>`, '')}

            <button type="submit">Ответить</button>
        </form>
        <details>
            <summary>Показать правильный ответ</summary>
            <section>
                <h3>Правильный ответ: ${rightAnswer}</h3>
                <p>${explanation}</p>
            </section>
        </details>`

        // помещаем шаблон в контейнер
        div.innerHTML = template

        // находим форму
        const form = div.querySelector('form')

        // выбираем первый инпут
        form.querySelector('input').setAttribute('checked', '')

        // обрабатываем отправку формы
        form.addEventListener('submit', ev => {
            // предотвращаем перезагрузку страницы
            ev.preventDefault()

            // определяем выбранный вариант ответа
            const chosenAnswer = form.querySelector('input:checked').value.substr(0, 1)

            // проверяем ответ
            checkAnswer(chosenAnswer, rightAnswer)
        })
    }

    function checkAnswer(chosenAnswer, rightAnswer) {
        // индикатор правильного ответа
        let isRight = true

        // если выбранный ответ совпадает с правильным,
        // увеличиваем количество правильных ответов,
        // записываем количество правильных ответов в локальное хранилище,
        // иначе увеличиваем количество неправильных ответов,
        // записываем количество неправильных ответов в локальное хранилище
        // и присваиваем индикатору false
        if (chosenAnswer === rightAnswer) {
            rightAnswers++
            localStorage.setItem('rightAnswers', rightAnswers)
        } else {
            wrongAnswers++
            localStorage.setItem('wrongAnswers', wrongAnswers)
            isRight = false
        }

        // находим кнопку
        const button = div.querySelector('button')

        // если ответ был правильным
        if (isRight) {
            // сообщаем об этом
            title.innerHTML = `<h1 class="right">Верно!</h1>`

            // выключаем кнопку
            button.disabled = true

            // через секунду вызываем функции
            // обновления рейтинга и прогресса и рендеринга следующего вопроса
            // отключаем таймер
            const timer = setTimeout(() => {
                updateScoreAndProgress()
                showQuestion()
                clearTimeout(timer)
            }, 1000)

            // если ответ был неправильным
        } else {
            // сообщаем об этом
            title.innerHTML = `<h1 class="wrong">Неверно!</h1>`

            // выключаем инпуты
            div.querySelectorAll('input').forEach(input => input.disabled = true)

            // раскрываем объяснение
            div.querySelector('details').setAttribute('open', '')

            // меняем текст кнопки
            button.textContent = 'Понятно'

            // по клику на кнопке вызываем функции
            // обновления рейтинга и прогресса и рендеринга следующего вопроса
            // удаляем обработчик
            button.addEventListener('click', () => {
                updateScoreAndProgress()
                showQuestion()
            }, {
                once: true
            })
        }

        // увеличиваем значение счетчика
        i++

        // записываем значение счетчика в локальное хранилище
        localStorage.setItem('i', i)
    }

    function updateScoreAndProgress() {
        // обновляем рейтинг
        score.innerHTML = `<span class="right">${rightAnswers}</span> - <span class="wrong">${wrongAnswers}</span>`

        // обновляем прогресс
        progress.innerHTML = `${i + 1} / ${assets.length}`
    }

    function showResult() {
        // определяем процент правильных ответов
        const percent = (rightAnswers / assets.length * 100).toFixed()

        // объявляем переменную для результата
        let result

        // в зависимости от процента правильных ответов
        // присваиваем result соответствующее значение
        if (percent >= 80) {
            result = `Отличный результат! Вы прекрасно знаете JavaScript.`
        } else if (percent > 50) {
            result = `Неплохой результат, но есть к чему стремиться.`
        } else {
            result = `Вероятно, вы только начали изучать JavaScript.`
        }

        // рендерим результаты
        B.innerHTML = `
        <h1>Ваш результат</h1>
        <div>
            <p>Правильных ответов: <span class="right">${rightAnswers}</span></p>
            <p>Неправильных ответов: <span class="wrong">${wrongAnswers}</span></p>
            <p>Процент правильных ответов: ${percent}</p>
            <p>${result}</p>
            <button>Заново</button>
        </div>
        `

        // при нажатии на кнопку
        // очищаем хранилище
        // и перезагружаем страницу,
        // удаляем обработчик
        B.querySelector('button').addEventListener('click', () => {
            localStorage.clear()
            location.reload()
        }, {
            once: true
        })
    }
})(document, document.body)


Благодарю за внимание, друзья.

Продолжение следует…