MakeFirstStep· Frontend + ИИ

Урок 19 · Инструменты и финал

Git и GitHub + отладка кода

Любой проект в команде живёт в Git, а любой код однажды ломается. Сегодня вы научитесь сохранять историю работы, предлагать изменения через pull request, разрешать конфликты и спокойно искать баги — вместо слов «у меня не работает».

  • Делать коммиты в понятной истории и писать сообщения по Conventional Commits
  • Работать с ветками и проходить цикл «ветка → push → pull request → ревью → merge»
  • Разрешать конфликт слияния и настраивать .gitignore
  • Ловить и бросать ошибки: try / catch / finally, throw, типы ошибок
  • Читать стек вызовов, ставить breakpoint в DevTools и смотреть вкладку Network
  • Описывать баг так, чтобы ИИ (и человек) смог помочь: минимальный воспроизводимый пример
Как работать с этой страницей

Блоки «Терминал» — это команды для вашего компьютера, в браузере они не запускаются: набирайте их в терминале VS Code. Код на JavaScript с кнопкой ▶ Запустить — живой: его можно менять и проверять прямо здесь. Git мы моделируем обычными массивами и объектами, так что логику add, commit, веток и конфликтов можно потрогать без установки. Подсказки к задачам спрятаны: сначала попробуйте сами.

01 Git: история проекта

Представьте, что вы делаете сайт для чойхоны. Сегодня всё работает, завтра вы «немного поправили» меню, и сайт сломался. Как вернуться? Копировать папку с названиями site-final, site-final-2, site-final-REAL? Так делают новички. Профессионалы используют Git — систему контроля версий. Она записывает каждое сохранённое состояние проекта, показывает, что и кто изменил, и позволяет вернуться к любому моменту.

Аналогия: семейный альбом

Вы выбираете из телефона снимки со свадьбы и кладёте их в конверт — это git add. Потом вклеиваете конверт в альбом и подписываете «Никоҳ, 12 июня» — это git commit. Альбом хранит все страницы по порядку. Захотите — откроете любую страницу и увидите, как всё выглядело тогда.

Три зоны и четыре места

Файл в Git проходит три зоны на вашем компьютере. Четвёртое место — сервер GitHub, куда вы отправляете копию.

Рабочая папкафайлы, которые вы правите в редакторе
→
Индекс (staging)«конверт»: что войдёт в следующий коммитgit add
→
Репозиторий«альбом»: сохранённая история на вашем компьютереgit commit
→
GitHub (remote)копия в интернете, общая для командыgit push
Путь изменения: правка → git add → git commit → git push

Первый репозиторий

Один раз на компьютере нужно представиться — это имя попадёт в каждый коммит:

Терминал
git config --global user.name "Ваше Имя"
git config --global user.email "you@example.com"

Теперь создадим проект и сделаем первый коммит:

Терминал
mkdir chaykhona-site
cd chaykhona-site
git init -b main
echo "# Чойхона" > README.md
git status
git add README.md
git commit -m "docs: add readme"
git log --oneline

Что произошло по шагам:

  1. git init создал скрытую папку .git — в ней живёт вся история. Удалите её, и проект перестанет быть репозиторием.
  2. git status сказал: файл README.md «untracked» — Git его ещё не отслеживает.
  3. git add положил файл в индекс («конверт»).
  4. git commit -m "…" записал снимок с подписью.
  5. git log --oneline показал историю: короткий код коммита и подпись.

Команды, которые вы будете использовать каждый день

КомандаЧто делает
git statusПоказывает, какие файлы изменены, какие в индексе, какие не отслеживаются
git add файл / git add .Кладёт файл (или все изменения) в индекс
git commit -m "текст"Сохраняет индекс как коммит с подписью
git log --onelineИстория коротко, по одной строке на коммит
git diffЧто изменено, но ещё не в индексе
git diff --stagedЧто лежит в индексе и войдёт в коммит
git restore файлОтменяет несохранённые правки файла (вернёт его к последнему коммиту)
git restore --staged файлДостаёт файл из индекса, правки остаются в папке
Частые ошибки

1. git add . не глядя: в коммит попадают лишние файлы (пароли, node_modules). Сначала git status, потом git add.

2. git commit без -m открывает текстовый редактор Vim. Если застряли: нажмите Esc, введите :q! и Enter. Либо всегда пишите -m.

3. Один гигантский коммит «всё за неделю». Коммит — это одна законченная мысль: «добавил фильтр», «починил итоговую сумму».

Чтобы почувствовать, как устроены зоны, смоделируем их кодом. Это первая задача на странице.

Практика: закрепляем

Мы моделируем Git обычным JavaScript — так вы увидите, что внутри нет магии.

ЗАДАЧА 1.1

Три зоны Git в коде

лёгкая

Мы смоделируем три зоны Git обычными массивами. Допишите add (git add: файл переезжает из work в staged) и commit (git commit: всё из staged уходит в новый коммит, а staged очищается). История выводится как в git log — новые коммиты сверху.

Можно редактировать · Ctrl + Enter
const repo = {
  work: ["index.html", "style.css", "app.js"], // изменённые файлы
  staged: [],                                  // подготовлены: git add
  commits: [],                                 // история: git commit
};

function add(file) {
  // убрать файл из work и положить в staged
}

function commit(message) {
  // сохранить файлы из staged в новый коммит { message, files }
  // и очистить staged
}

add("index.html");
add("style.css");
commit("add page skeleton");
add("app.js");
commit("add order counter");

for (let i = repo.commits.length - 1; i >= 0; i--) {
  const c = repo.commits[i];
  console.log(`${i + 1}. ${c.message} (${c.files.join(", ")})`);
}
console.log(repo.work.length, repo.staged.length);
Ожидаемый результат
2. add order counter (app.js)
1. add page skeleton (index.html, style.css)
0 0
Подсказка 1

В add нужны две вещи: убрать файл из work (например, через filter) и сделать push в staged.

Подсказка 2

В commit сохраните копию списка: files: [...repo.staged]. Потом repo.staged = []. Привычка копировать пригодится в разделе 8 — там мутация ломает программы.

ЗАДАЧА 1.2

Что покажет git status

лёгкая

Функция statusLine описывает файл так, как это делает git status. Но один случай она не знает: файл уже в индексе (staged), а после git add его ещё раз изменили (changedAfterAdd: true). Для него нужна строка staged, but changed again: имя. Допишите условие.

Можно редактировать · Ctrl + Enter
const files = [
  { name: "index.html", tracked: true, changed: false, staged: false },
  { name: "app.js", tracked: true, changed: true, staged: false },
  { name: "notes.txt", tracked: false, changed: true, staged: false },
  { name: "style.css", tracked: true, changed: true, staged: true, changedAfterAdd: false },
  { name: "README.md", tracked: true, changed: true, staged: true, changedAfterAdd: true },
];

function statusLine(f) {
  if (!f.tracked) return `untracked: ${f.name}`;
  if (f.staged) return `to be committed: ${f.name}`;
  if (f.changed) return `modified: ${f.name}`;
  return null; // чистый файл в статусе не показывается
}

files.map(statusLine).filter(Boolean).forEach((line) => console.log(line));
Ожидаемый результат
modified: app.js
untracked: notes.txt
to be committed: style.css
staged, but changed again: README.md
Подсказка 1

Новую проверку нужно поставить перед обычной проверкой f.staged: иначе сработает она, и до вашего условия дело не дойдёт.

Подсказка 2

if (f.staged && f.changedAfterAdd) return `staged, but changed again: ${f.name}`;

Читать подробнее: Pro Git — запись изменений в репозиторий.

02 Ветки: работаем, не ломая основное

В репозитории есть главная ветка — обычно main (иногда её называют master). В ней лежит версия, которая «точно работает». Новую функцию делают в отдельной ветке: можно пробовать, ошибаться и коммитить сколько угодно — основная ветка не пострадает. Когда функция готова и проверена, ветку вливают обратно.

Аналогия: пробный плов

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

init header footer fix typo merge release filters sort tests main feature/filters здесь ветка отошла
Зелёная линия — main, синяя — ветка feature/filters. Пока вы работаете в ветке, main живёт своей жизнью, потом ветки сливаются коммитом merge

Ветка — это не копия папки. Это всего лишь подвижная «закладка» на один из коммитов. Новый коммит двигает закладку вперёд. Благодаря этому ветки в Git создаются мгновенно и стоят почти ничего. Особая закладка HEAD показывает, на какой ветке вы сейчас стоите.

Терминал
git branch                         # список веток, звёздочка у текущей
git switch -c feature/filters      # создать ветку и перейти на неё
# ...правим файлы, git add, git commit...
git log --oneline --graph --all    # история в виде дерева
git switch main                    # вернуться на main
git merge feature/filters          # влить ветку в main
git branch -d feature/filters      # удалить ветку, она больше не нужна

Как называть ветки

В командах ветку называют по шаблону тип/короткое-описание: feature/order-filters, fix/wrong-total, docs/readme-tajik. Английские слова, строчные буквы, без пробелов, слова через дефис. По имени сразу видно, что в ветке и зачем.

Частые ошибки

Работать прямо в main. Тогда PR сделать не из чего, а в случае проблемы нечем откатиться. Привычка: сначала git switch -c, потом код.

Переключать ветку с несохранёнными правками. Git либо перенесёт их, либо откажется. Лучше сначала сделать коммит.

Ветки — это граф коммитов, и мы можем обойти его кодом. Начнём с простого: пройдём цепочку «родитель → родитель», как это делает git log.

Практика: закрепляем

ЗАДАЧА 2.1

История ветки

лёгкая

Каждый коммит помнит своего «родителя». Ветка — это просто указатель на последний коммит. Допишите history(branch): пройдите от конца ветки по цепочке parent и верните массив сообщений от новых к старым — так работает git log.

Можно редактировать · Ctrl + Enter
const commits = [
  { id: "c1", parent: null, message: "init project" },
  { id: "c2", parent: "c1", message: "add header" },
  { id: "c3", parent: "c2", message: "add footer" },   // конец main
  { id: "f1", parent: "c2", message: "add filters" },  // ветка feature отошла от c2
  { id: "f2", parent: "f1", message: "add sort" },     // конец feature
];
const branches = { main: "c3", feature: "f2" };
const byId = Object.fromEntries(commits.map((c) => [c.id, c]));

function history(branch) {
  // начните с branches[branch] и идите по parent, пока он не станет null
}

console.log(history("main").join(" <- "));
console.log(history("feature").join(" <- "));
Ожидаемый результат
add footer <- add header <- init project
add sort <- add filters <- add header <- init project
Подсказка 1

Заведите let id = branches[branch] и цикл while (id !== null). В теле: достаньте коммит byId[id], сохраните его message, перейдите к parent.

Подсказка 2

Не забудьте менять id в каждом витке, иначе цикл станет бесконечным.

ЗАДАЧА 2.2

Что есть только в моей ветке

средняя

Перед pull request полезно знать: какие коммиты есть в вашей ветке, но ещё не попали в основную? В Git это записывают как main..feature. Функция history уже готова. Допишите onlyIn(branch, base): сообщения коммитов из branch, которых нет в base.

Можно редактировать · Ctrl + Enter
const commits = [
  { id: "c1", parent: null, message: "init project" },
  { id: "c2", parent: "c1", message: "add header" },
  { id: "c3", parent: "c2", message: "add footer" },   // конец main
  { id: "f1", parent: "c2", message: "add filters" },  // ветка feature отошла от c2
  { id: "f2", parent: "f1", message: "add sort" },     // конец feature
];
const branches = { main: "c3", feature: "f2" };
const byId = Object.fromEntries(commits.map((c) => [c.id, c]));

function history(branch) {
  const result = [];
  let id = branches[branch];
  while (id !== null) {
    result.push(byId[id].message);
    id = byId[id].parent;
  }
  return result;
}

function onlyIn(branch, base) {
  // вернуть массив сообщений
}

console.log(onlyIn("feature", "main").join(", "));
console.log(onlyIn("main", "feature").join(", "));
console.log(onlyIn("main", "main").length);
Ожидаемый результат
add sort, add filters
add footer
0
Подсказка 1

Получите историю обеих веток и оставьте в первой только то, чего нет во второй: filter + includes.

Подсказка 2

return history(branch).filter((m) => !history(base).includes(m));

Читать подробнее: Pro Git — о ветвлении в двух словах.

03 GitHub: push, pull request, ревью

Git живёт на вашем компьютере. GitHub — это сайт, где хранится копия репозитория (её называют origin — «удалённый репозиторий»). Именно через GitHub команда обменивается кодом, а работодатель смотрит ваше портфолио.

Терминал
git remote add origin https://github.com/ВАШ_ЛОГИН/chaykhona-site.git
git push -u origin main            # первая отправка, -u запоминает связь веток
git push                           # дальше достаточно просто push
git clone https://github.com/ВАШ_ЛОГИН/chaykhona-site.git   # скачать чужой репозиторий
git pull                           # забрать новые коммиты с GitHub
git fetch                          # только скачать информацию, ничего не менять

git pull — это fetch (скачать) + merge (влить в вашу ветку). Если на GitHub появились новые коммиты коллег, а у вас нет, перед push сначала делают pull.

Pull request — главная идея командной работы

Pull request (PR) — это просьба: «посмотрите мои изменения в ветке и, если всё хорошо, влейте их в main». PR — не команда Git, а возможность GitHub. Вы пушите ветку, на сайте нажимаете «Compare & pull request», пишете описание, а коллега читает изменения построчно, оставляет комментарии, одобряет или просит правки.

Аналогия: чек перед оплатой на бозоре

Продавец записал сумму на бумажке, но перед оплатой вы показываете бумажку соседу: «Проверь, правильно ли сложил?». Сосед нашёл ошибку в одну сомони — вы исправили, и только потом отдали деньги. PR — тот же «взгляд со стороны» перед тем, как код попадёт в основную ветку.

Рабочий цикл в паре

Именно так вы будете работать сегодня на занятии и дальше — в проектах и на работе:

  1. Обновите main: git switch main, git pull.
  2. Создайте ветку под одну задачу: git switch -c feature/order-filters.
  3. Работайте и коммитьте небольшими осмысленными шагами.
  4. Отправьте ветку: git push -u origin feature/order-filters. GitHub напечатает ссылку для создания PR.
  5. Откройте pull request: заголовок, что сделано, зачем, как проверить. Назначьте напарника ревьюером.
  6. Ревью: напарник читает diff, пишет комментарии. Вы отвечаете и правите — новые коммиты в ту же ветку попадут в PR сами.
  7. Merge: после одобрения (approve) и успешных проверок PR вливают в main кнопкой Merge pull request на GitHub.
  8. Уберите за собой: git switch main, git pull, git branch -d feature/order-filters.
Что смотреть при ревью

Работает ли код (запустите ветку)? Понятны ли имена? Нет ли лишних файлов и console.log? Описаны ли граничные случаи (пустой список, ноль, ошибка сети)? Комментарий пишем про код, а не про человека: «Здесь при пустом списке будет ошибка» вместо «Ты забыл».

Три способа влить PR

КнопкаЧто получится в истории
Create a merge commitВсе коммиты ветки сохраняются, плюс коммит слияния
Squash and mergeВсе коммиты PR склеиваются в один — история main короче
Rebase and mergeКоммиты ветки переносятся в main по одному, без коммита слияния

Для начала подойдёт первый вариант. Остальные вы выберете, когда в команде договорятся о стиле истории.

Правила безопасности

Не пушьте в main напрямую и не сливайте свой PR без ревью. Во многих командах main закрыт: только через PR.

Не используйте git push --force, пока не понимаете, что делаете: команда стирает чужие коммиты на сервере.

Никогда не коммитьте пароли и ключи API. Разберём в разделе про .gitignore.

Практика: закрепляем

ЗАДАЧА 3.1

Описание pull request

средняя

Хорошее описание PR экономит ревьюеру время. Допишите buildPr(pr): функция возвращает одну строку в формате из «Ожидаемого результата»: заголовок «Что сделано» с маркированным списком, пустая строка, «Зачем», пустая строка, «Как проверить» с нумерованным списком.

Можно редактировать · Ctrl + Enter
function buildPr(pr) {
  // верните строку; соберите массив строк и вызовите join("\n")
}

const pr = {
  what: ["добавлен фильтр заказов по статусу", "фильтр запоминается в localStorage"],
  why: "Менеджер хочет быстро находить неоплаченные заказы.",
  check: ["Откройте страницу заказов", "Выберите статус «Не оплачен»", "Обновите страницу: фильтр остался"],
};
console.log(buildPr(pr));
Ожидаемый результат
## Что сделано
- добавлен фильтр заказов по статусу
- фильтр запоминается в localStorage

## Зачем
Менеджер хочет быстро находить неоплаченные заказы.

## Как проверить
1. Откройте страницу заказов
2. Выберите статус «Не оплачен»
3. Обновите страницу: фильтр остался
Подсказка 1

Начните с массива const lines = ["## Что сделано"]. Пункты добавляйте через forEach: lines.push(`- ${w}`). Пустая строка в массиве — это просто "".

Подсказка 2

Нумерация: pr.check.forEach((c, i) => lines.push(`${i + 1}. ${c}`)). В конце — lines.join("\n").

ЗАДАЧА 3.2

Можно ли делать merge

сложная

На GitHub кнопка Merge «серая», пока не выполнены условия. Допишите mergeProblems(pr): функция возвращает массив причин (в таком порядке): «нет одобрения» (approvals < 1), «проверки не прошли» (checks не равно "passed"), «есть конфликт» (conflicts), «это черновик» (draft). Если причин нет — пустой массив.

Можно редактировать · Ctrl + Enter
function mergeProblems(pr) {
  const problems = [];
  // проверьте четыре условия и добавляйте причины в массив
  return problems;
}

const prs = [
  { title: "add order filters", approvals: 1, checks: "passed", conflicts: false, draft: false },
  { title: "fix cart total", approvals: 0, checks: "passed", conflicts: false, draft: false },
  { title: "wip: new header", approvals: 2, checks: "failed", conflicts: true, draft: true },
];
for (const pr of prs) {
  const p = mergeProblems(pr);
  console.log(`${pr.title}: ${p.length ? p.join("; ") : "можно мержить"}`);
}
Ожидаемый результат
add order filters: можно мержить
fix cart total: нет одобрения
wip: new header: проверки не прошли; есть конфликт; это черновик
Подсказка 1

Четыре независимых if, каждый делает problems.push("…"). else здесь не нужен: причин может быть несколько сразу.

Подсказка 2

Для проверок используйте pr.approvals < 1 и pr.checks !== "passed".

Читать подробнее: GitHub Docs — о pull request, Pro Git — удалённые репозитории.

04 Конфликты слияния

Git умеет сливать изменения автоматически, если вы и напарник правили разные места. Но если вы оба изменили одну и ту же строку по-разному, Git не знает, чья версия верна, и просит человека решить. Это и есть конфликт. Он не поломка, а обычная часть работы: в любой команде конфликты случаются каждую неделю.

Допустим, вы с напарником правили файл rate.js. Напарник уже влил свою ветку в main, а вы сделали git pull origin main в своей ветке:

Терминал
git switch feature/rate
git pull origin main
# Auto-merging rate.js
# CONFLICT (content): Merge conflict in rate.js
# Automatic merge failed; fix conflicts and then commit the result.

Git остановился и записал в файл маркеры конфликта:

rate.js после неудачного merge
// курс обмена
<<<<<<< HEAD
const convertUsd = (usd, rate = 10.9) => Math.round(usd * rate);
=======
const toSomoni = (usd) => usd * 11;
>>>>>>> origin/main
<<<<<<< HEAD
Начало конфликта. Ниже — ваша версия (из ветки, на которой вы стоите).
=======
Разделитель. Ниже — версия из другой ветки (здесь origin/main).
>>>>>>> origin/main
Конец конфликта и имя ветки, откуда пришла вторая версия.

Как разрешить конфликт

  1. Откройте файл со словом CONFLICT. В VS Code над блоком появятся кнопки: Accept Current (моё), Accept Incoming (чужое), Accept Both (оба) и Compare.
  2. Решите по смыслу, что должно остаться. Иногда нужно взять кусочки из обеих версий — и написать итоговый код руками. Договоритесь с автором второй правки.
  3. Удалите все три строки маркеров. В файле должен остаться только нормальный код.
  4. Запустите проект и убедитесь, что всё работает.
  5. Завершите слияние:
    Терминал
    git add rate.js
    git commit -m "merge main into feature/rate"
    git push
Если запутались

Команда git merge --abort (или git rebase --abort) отменяет слияние и возвращает всё, как было до него. Конфликт решают в своей ветке до merge: тогда в main попадёт уже чистый результат.

Опасная ошибка

Закоммитить файл, в котором остались строки <<<<<<<. Такой код не запустится. Перед git add поищите в проекте <<<<<<< (Ctrl+Shift+F в VS Code).

Маркеры — это обычный текст, и разобрать их можно кодом. Первую задачу вы решите руками, как в настоящем конфликте. Во второй напишете «автоматическое разрешение», как это делают редакторы.

Практика: закрепляем

ЗАДАЧА 4.1

Решаем конфликт руками

средняя

После неудачного merge файл выглядит как в переменной conflicted. Команда решила: имя функции оставляем из main (toSomoni), а формулу берём из ветки feature/rate: параметр rate со значением по умолчанию 10.9 и округление. Впишите в resolved итоговый текст файла: без маркеров и с одной строкой кода. Четыре проверки ниже должны выдать false / true / true / true.

Можно редактировать · Ctrl + Enter
const conflicted = `// курс обмена
<<<<<<< HEAD
const toSomoni = (usd) => usd * 11;
=======
const convertUsd = (usd, rate = 10.9) => Math.round(usd * rate);
>>>>>>> feature/rate
`;

const resolved = `// курс обмена
`; // допишите итоговую строку кода

console.log("маркеры остались:", /<<<<<<<|=======|>>>>>>>/.test(resolved));
console.log("имя из main:", resolved.includes("const toSomoni ="));
console.log("формула из feature:", resolved.includes("rate = 10.9") && resolved.includes("Math.round(usd * rate)"));
console.log("лишнее убрано:", !resolved.includes("convertUsd") && !resolved.includes("usd * 11"));
Ожидаемый результат
маркеры остались: false
имя из main: true
формула из feature: true
лишнее убрано: true
Подсказка 1

Итог — одна строка: const toSomoni = (usd, rate = 10.9) => Math.round(usd * rate);. Строки с <<<<<<<, ======= и >>>>>>> в файле остаться не должны.

Подсказка 2

Проверка ищет в тексте точные куски: const toSomoni =, rate = 10.9 и Math.round(usd * rate). Следите за пробелами и опечатками.

ЗАДАЧА 4.2

Автоматическое разрешение

сложная

Напишите resolveAll(text, side), которая убирает маркеры конфликта. side = "ours" оставляет версию из HEAD (до =======), "theirs" — версию из чужой ветки (после =======), "both" — обе подряд. Строки вне конфликта сохраняются как есть. Идите по строкам и помните, в какой части блока сейчас находитесь.

Можно редактировать · Ctrl + Enter
const conflicted = [
  "<h1>MakeFirstStep</h1>",
  "<<<<<<< HEAD",
  "<p>Курсы программирования</p>",
  "=======",
  "<p>Курсы Frontend и ИИ</p>",
  ">>>>>>> feature/text",
  "<footer>Душанбе</footer>",
].join("\n");

function resolveAll(text, side) {
  const out = [];
  let part = "none"; // none | ours | theirs
  for (const line of text.split("\n")) {
    // ваш код
  }
  return out.join("\n");
}

for (const side of ["ours", "theirs", "both"]) {
  console.log(side + ": " + resolveAll(conflicted, side).split("\n").join(" / "));
}
Ожидаемый результат
ours: <h1>MakeFirstStep</h1> / <p>Курсы программирования</p> / <footer>Душанбе</footer>
theirs: <h1>MakeFirstStep</h1> / <p>Курсы Frontend и ИИ</p> / <footer>Душанбе</footer>
both: <h1>MakeFirstStep</h1> / <p>Курсы программирования</p> / <p>Курсы Frontend и ИИ</p> / <footer>Душанбе</footer>
Подсказка 1

Три «переключателя»: строка, начинающаяся с <<<<<<< , включает режим "ours"; строка ======= — "theirs"; строка с >>>>>>> возвращает "none". Сами маркеры в результат не попадают.

Подсказка 2

Строку вне конфликта (part === "none") добавляем всегда; строку внутри — только если part === side или side === "both".

Читать подробнее: Pro Git — основы ветвления и слияния (конфликты).

05 .gitignore и сообщения коммитов

.gitignore: что не должно попасть в репозиторий

В папке проекта бывают файлы, которые нельзя или не нужно хранить в Git. Их перечисляют в файле .gitignore в корне проекта: Git не будет их отслеживать и не покажет в git status.

.gitignore
# зависимости: восстанавливаются командой npm install
node_modules/

# готовая сборка: создаётся командой npm run build
dist/

# секреты: пароли, ключи API
.env

# логи и системный мусор
*.log
.DS_Store
имя/
Папка с этим именем, где бы она ни лежала: dist/, node_modules/.
*.расширение
Все файлы с таким окончанием: *.log.
имя
Файл с точно таким именем: .env. Файл .env.example под это правило не попадёт.
!имя
Исключение: вернуть файл под контроль Git. Стоит после общего правила.
# текст
Комментарий.

Настоящий .gitignore поддерживает ещё много синтаксиса (**, пути со слэшем), но этого хватит почти всегда. В задаче ниже вы пользуетесь именно таким упрощённым «движком».

Самая дорогая ошибка

Закоммитили .env с ключом, а потом добавили его в .gitignore. Не помогает: файл уже в истории. Любой, кто откроет репозиторий, найдёт ключ в старом коммите. Правильно: сразу отозвать (сменить) ключ у сервиса и только потом чистить репозиторий. Если файл уже отслеживается, его можно убрать из Git командой git rm --cached .env — на диске он останется.

Сообщения коммитов: Conventional Commits

Через полгода вы не вспомните, что значило «update» или «fix». Поэтому в командах договариваются о формате: тип(область): что сделано. Область необязательна.

ТипКогдаПример
featновая возможностьfeat: add order filters
fixисправление багаfix(cart): count first item in total
docsдокументация, READMEdocs: add install steps
styleформатирование без смыслаstyle: format with prettier
refactorпереписали, поведение то жеrefactor: extract calcTotal
testтестыtest: cover empty cart
choreслужебное: зависимости, настройкиchore: bump deps
Плохо

update, fix, changes, asdf, final, Fixed bug. Непонятно что, где и зачем; нельзя найти нужный коммит.

Хорошо

fix: wrong total in cart, feat(ui): add dark theme. Глагол в повелительном наклонении («add», «fix»), строчные буквы, до 50 символов, без точки, одна мысль на коммит.

Чтобы закрепить, посмотрим, как классификация сообщений выглядит в коде. Запустите пример и поменяйте сообщения:

Тип коммита по сообщению · Ctrl + Enter
const messages = ["feat: add filters", "fix(cart): sum", "update", "docs: readme"];

for (const m of messages) {
  const match = m.match(/^([a-z]+)(\([^)]*\))?: /);
  console.log(m.padEnd(20), "->", match ? match[1] : "нет типа");
}

Практика: закрепляем

ЗАДАЧА 5.1

Пишем .gitignore

средняя

Ниже — упрощённый «движок» .gitignore (он понимает имя/, *.расширение, точное имя и !отмену). Впишите в массив rules такие правила, чтобы игнорировались папки node_modules, dist, coverage, файл .env (но не .env.example), системный .DS_Store и все *.log, кроме keep.log. Остальное должно остаться под контролем Git.

Можно редактировать · Ctrl + Enter
function matchRule(rule, path) {
  const parts = path.split("/");
  const name = parts[parts.length - 1];
  if (rule.endsWith("/")) {                          // «папка/»: любой каталог с таким именем
    return parts.slice(0, -1).includes(rule.slice(0, -1));
  }
  if (rule.startsWith("*.")) return name.endsWith(rule.slice(1)); // «*.log»: по расширению
  return name === rule;                              // точное имя файла
}

function isIgnored(path, rules) {
  let ignored = false;
  for (const rule of rules) {                        // правила читаются сверху вниз
    if (rule.startsWith("!")) {                      // «!имя» возвращает файл обратно
      if (matchRule(rule.slice(1), path)) ignored = false;
    } else if (matchRule(rule, path)) {
      ignored = true;
    }
  }
  return ignored;
}

const paths = [
  "node_modules/react/index.js", "dist/app.js", "src/app.js", ".env", ".env.example",
  "debug.log", "logs/server.log", "keep.log", "README.md", ".DS_Store", "src/.DS_Store", "coverage/index.html",
];

const rules = [
  // впишите правила строками
];

console.log("игнорируются:", paths.filter((p) => isIgnored(p, rules)).join(", "));
console.log("остаются:", paths.filter((p) => !isIgnored(p, rules)).join(", "));
Ожидаемый результат
игнорируются: node_modules/react/index.js, dist/app.js, .env, debug.log, logs/server.log, .DS_Store, src/.DS_Store, coverage/index.html
остаются: src/app.js, .env.example, keep.log, README.md
Подсказка 1

Папки пишутся со слэшем на конце: "dist/". Расширение — со звёздочкой: "*.log". Файл — точным именем: ".env".

Подсказка 2

Исключение обязано стоять после общего правила: "*.log", "!keep.log". Побеждает последнее подходящее правило.

ЗАДАЧА 5.2

Статистика коммитов

лёгкая

В формате Conventional Commits сообщение начинается с типа: feat: …, fix(cart): …. Допишите commitType(msg): она возвращает тип (слово до двоеточия или до скобки) либо "other", если сообщение не по формату. Подсчёт и вывод уже готовы.

Можно редактировать · Ctrl + Enter
const messages = [
  "feat: add order filters",
  "fix: wrong total in cart",
  "docs: update readme",
  "fix(cart): off-by-one in loop",
  "update",
  "feat(ui): dark theme",
  "fix: crash on empty order",
  "Changes",
];

function commitType(msg) {
  // регулярка: слово в начале, необязательная (область), двоеточие и пробел
  return "other";
}

const counts = {};
for (const m of messages) {
  const type = commitType(m);
  counts[type] = (counts[type] || 0) + 1;
}
console.log(JSON.stringify(counts));
Ожидаемый результат
{"feat":2,"fix":3,"docs":1,"other":2}
Подсказка 1

Регулярка: /^([a-z]+)(\([^)]*\))?: /. Первая группа — тип, вторая (необязательная) — область в скобках.

Подсказка 2

const m = msg.match(regex); return m ? m[1] : "other";

ЗАДАЧА 5.3

Линтер коммитов

сложная

Напишите lintCommit(msg) — массив проблем. Правила: (1) сообщение должно начинаться с feat|fix|docs|style|refactor|test|chore, необязательной (области) и : ; иначе одна проблема «нет типа вида feat: …» — дальше можно не проверять; (2) «тема длиннее 50 символов» (темой считаем текст после : ); (3) «тема с заглавной буквы»; (4) «точка в конце». Пустой массив значит «всё хорошо».

Можно редактировать · Ctrl + Enter
function lintCommit(msg) {
  const problems = [];
  return problems;
}

const list = [
  "feat: add order filters",
  "Fixed bug.",
  "fix(cart): Wrong total.",
  "docs: describe in the readme how to install dependencies and run the project",
];
for (const m of list) {
  const p = lintCommit(m);
  console.log(`${m.slice(0, 24)} -> ${p.length ? p.join("; ") : "ok"}`);
}
Ожидаемый результат
feat: add order filters -> ok
Fixed bug. -> нет типа вида feat: …
fix(cart): Wrong total. -> тема с заглавной буквы; точка в конце
docs: describe in the re -> тема длиннее 50 символов
Подсказка 1

Одна регулярка с тремя группами: /^(feat|fix|docs|style|refactor|test|chore)(\([^)]*\))?: (.+)$/. Тема — это m[3].

Подсказка 2

Если match вернул null — сразу return ["нет типа вида feat: …"]. Остальные проверки — три независимых if с push.

Читать подробнее: Conventional Commits, документация .gitignore.

06 Ошибки: try / catch / finally и throw

Ошибки случаются у всех: пользователь ввёл пустую строку, сервер не ответил, из localStorage пришёл мусор. Если ничего не делать, программа остановится на первой же строке с ошибкой. Задача разработчика — заранее предусмотреть места, где код может упасть, и сделать так, чтобы пользователь увидел понятное сообщение, а не белый экран.

try / catch / finally

Аналогия: страховка в маршрутке

try — «попробуем проехать по этой дороге». catch — запасной план, если дорога перекрыта: свернуть и поехать в объезд. finally — то, что произойдёт в любом случае: выйти из маршрутки и заплатить водителю.

Три блока · Ctrl + Enter
function readAge(text) {
  try {
    const age = JSON.parse(text);          // может бросить ошибку
    console.log("прочитали:", age);
    return age;
  } catch (error) {
    console.log("не получилось:", error.name);
    return 0;                              // запасной план
  } finally {
    console.log("— чтение закончено —");   // выполняется всегда
  }
}

console.log(readAge("19"));
console.log(readAge("девятнадцать"));

Ошибка внутри try немедленно прерывает его, и управление уходит в catch. Оставшиеся строки try пропускаются. В catch приходит объект ошибки (его обычно называют error или e).

Что внутри объекта ошибки

name
Тип ошибки: TypeError, ReferenceError, SyntaxError, RangeError…
message
Человеческое описание: «Cannot read properties of null».
stack
Стек вызовов — цепочка функций, которые привели к ошибке. Разберём в разделе 7.

Основные типы ошибок

ТипКогда возникаетПример
TypeErrorДействие невозможно для этого типа значения: читаем свойство у null / undefined, вызываем не функциюnull.name
ReferenceErrorПеременная не существует (опечатка, не объявлена, вне области видимости)console.log(pirce)
SyntaxErrorКод написан не по правилам языка; также ошибки JSON.parseJSON.parse("{a: 1}")
RangeErrorЗначение вне допустимого диапазонаnew Array(-1)
Смотрим на объект ошибки · Ctrl + Enter
try {
  const user = null;
  console.log(user.name);
} catch (error) {
  console.log("name:", error.name);
  console.log("message:", error.message);
  console.log("есть stack:", typeof error.stack === "string");
}

throw: бросаем свою ошибку

Иногда ошибку нужно создать самому. Например, функция получила от пользователя отрицательное количество. throw прерывает текущую функцию и «бросает» ошибку вверх — до ближайшего catch. Бросайте объект ошибки (new Error("текст")), а не просто строку: у объекта есть стек.

throw new Error · Ctrl + Enter
function setQuantity(qty) {
  if (!Number.isInteger(qty)) throw new TypeError("Количество должно быть целым числом");
  if (qty < 1) throw new RangeError("Количество не может быть меньше 1");
  return qty;
}

for (const value of [3, 0, 2.5]) {
  try {
    console.log("ок:", setQuantity(value));
  } catch (e) {
    console.log(`${e.name}: ${e.message}`);
  }
}
Частая ошибка: пустой catch

Пустой catch (e) {} «проглатывает» ошибку: программа молчит и ведёт себя странно, а причину не найти. Минимум — выведите ошибку в консоль или покажите пользователю сообщение. И ловите только те ошибки, которые умеете обработать: остальные пробрасывайте дальше через throw e (задача 6.5).

Этот код выдаст ошибку · Ctrl + Enter
function risky() {
  throw new Error("что-то пошло не так");
}

risky();   // ошибка не поймана: выполнение остановилось
console.log("до этой строки мы уже не дойдём");

Ошибки в асинхронном коде ловят тем же try / catch вокруг await (вы видели это на уроке 13, а на следующих уроках будете так обрабатывать запросы fetch). Отдельный try вокруг setTimeout не сработает: колбэк выполнится позже, когда блок уже завершился.

Практика: закрепляем

ЗАДАЧА 6.1

Предскажи: порядок с finally

лёгкая

Сначала без запуска решите, в каком порядке появятся строки. Блок finally выполняется «всегда» — даже если в try уже был return. Впишите прогноз в prediction (строки по порядку, ровно как их показал бы console.log) и запустите. Менять остальной код не нужно.

Можно редактировать · Ctrl + Enter
const log = [];   // сюда код собирает «вывод»
function test() {
  try {
    log.push("1: try");
    return "результат";
  } catch (e) {
    log.push("catch");
  } finally {
    log.push("2: finally");
  }
  log.push("после");
}
log.push("3: " + test());

const prediction = [];   // ваш прогноз: строки по порядку
console.log(log.join("|") === prediction.join("|") ? "Верно!" : "Не угадали, подумайте ещё");
Ожидаемый результат
Верно!
Подсказка 1

Функция заканчивается на return, но значение «отдаётся» наружу только после того, как выполнится finally.

Подсказка 2

Строка «после» не появится: функция уже вернула значение. «catch» не появится: ошибки не было.

ЗАДАЧА 6.2

Имя ошибки

лёгкая

Допишите errorName(fn): вызовите функцию внутри try, а если она бросила ошибку — верните её name (например, "TypeError"). Если ошибки нет — верните "ok".

Можно редактировать · Ctrl + Enter
function errorName(fn) {
  // try / catch
}

const tests = [
  () => { const user = null; return user.name; },
  () => missingVariable + 1,
  () => JSON.parse("{name: 'Али'}"),
  () => new Array(-5),
  () => 2 + 2,
];
tests.forEach((t, i) => console.log(i + 1, errorName(t)));
Ожидаемый результат
1 TypeError
2 ReferenceError
3 SyntaxError
4 RangeError
5 ok
Подсказка 1

Структура: try { fn(); return "ok"; } catch (e) { return e.name; }.

Подсказка 2

Свойство name у объекта ошибки — это «тип»: TypeError, ReferenceError, SyntaxError, RangeError.

ЗАДАЧА 6.3

Безопасное чтение JSON

средняя

Данные из localStorage или с сервера бывают повреждены. Допишите safeParse(text, fallback): возвращает результат JSON.parse, а при ошибке выводит ошибка чтения: ИмяОшибки и возвращает fallback.

Можно редактировать · Ctrl + Enter
function safeParse(text, fallback) {
  // try / catch
}

console.log(safeParse('{"name":"Фарҳод","age":19}', null).name);
console.log(safeParse("{name: Фарҳод}", "пусто"));
console.log(safeParse("", []).length);
Ожидаемый результат
Фарҳод
ошибка чтения: SyntaxError
пусто
ошибка чтения: SyntaxError
0
Подсказка 1

В try — return JSON.parse(text). В catch — console.log("ошибка чтения:", e.name) и return fallback.

Подсказка 2

Пустая строка тоже невалидный JSON, поэтому для третьего вызова catch тоже сработает и вернёт пустой массив.

ЗАДАЧА 6.4

Свои ошибки через throw

средняя

Допишите withdraw(balance, amount) — снятие сомони. Если amount не больше нуля, бросьте new RangeError("Сумма должна быть больше нуля"). Если amount больше баланса — new Error("Недостаточно сомони"). Иначе верните остаток. Порядок проверок — как в условии.

Можно редактировать · Ctrl + Enter
function withdraw(balance, amount) {
  // две проверки с throw, затем return
}

for (const amount of [200, 0, 5000, -10]) {
  try {
    console.log("остаток:", withdraw(1000, amount));
  } catch (e) {
    console.log(`${e.name}: ${e.message}`);
  } finally {
    console.log("— операция завершена —");
  }
}
Ожидаемый результат
остаток: 800
— операция завершена —
RangeError: Сумма должна быть больше нуля
— операция завершена —
Error: Недостаточно сомони
— операция завершена —
RangeError: Сумма должна быть больше нуля
— операция завершена —
Подсказка 1

throw сразу прерывает функцию, поэтому после него else не нужен: if (amount <= 0) throw new RangeError("…");.

Подсказка 2

Блок finally в цикле выполнится после каждой операции — и успешной, и неудачной.

ЗАДАЧА 6.5

Ловим только свои ошибки

сложная

Хороший catch обрабатывает только те ошибки, которые ожидает, а чужие пробрасывает дальше: throw e. Допишите validate(order): если у заказа нет name, вызовите fail("name", "укажите имя"); если qty меньше 1 — fail("qty", "минимум 1 штука"). Вспомогательная функция fail уже есть. Вызов process(null) должен упасть настоящим TypeError, который process не глотает.

Можно редактировать · Ctrl + Enter
function fail(field, message) {
  const e = new Error(message);
  e.name = "ValidationError";
  e.field = field;
  throw e;
}

function validate(order) {
  // две проверки через fail(...)
}

function process(order) {
  try {
    validate(order);
    return "заказ принят";
  } catch (e) {
    if (e.name === "ValidationError") return `поле ${e.field}: ${e.message}`;
    throw e; // чужие ошибки не глотаем
  }
}

console.log(process({ name: "Мадина", qty: 2 }));
console.log(process({ name: "", qty: 2 }));
console.log(process({ name: "Рустам", qty: 0 }));
try {
  process(null);
} catch (e) {
  console.log("неожиданная ошибка:", e.name);
}
Ожидаемый результат
заказ принят
поле name: укажите имя
поле qty: минимум 1 штука
неожиданная ошибка: TypeError
Подсказка 1

if (!order.name) fail("name", "укажите имя");. Вторая проверка пишется так же, с условием order.qty < 1.

Подсказка 2

Для null первая же строка order.name бросит TypeError: у null нет свойств. Ваш validate этот случай специально обрабатывать не должен.

Читать подробнее: learn.javascript.ru — обработка ошибок, try…catch, MDN — try...catch.

07 Стек вызовов и DevTools

Как читать красную ошибку

Когда код падает, браузер печатает красную строку и стек вызовов: список функций от места ошибки до самого начала. Читайте его сверху вниз: первая строка — где упало, следующие — кто вызвал эту функцию.

Ошибка в консоли браузера
Uncaught TypeError: Cannot read properties of undefined (reading 'price')
    at calcTotal (cart.js:12:23)
    at renderCart (ui.js:48:15)
    at HTMLButtonElement.<anonymous> (main.js:7:3)
  1. Тип и сообщение: TypeError — читаем свойство price у undefined. Значит, где-то вместо товара пришёл undefined.
  2. Верхняя строка стека: calcTotal (cart.js:12:23) — функция calcTotal, файл cart.js, строка 12, символ 23. Здесь упало.
  3. Вторая строка: renderCart вызвала calcTotal. Часто баг не в верхней функции, а в том, что ей передали.
  4. Последняя строка: всё началось с клика по кнопке в main.js.

Клик по cart.js:12 в консоли DevTools откроет этот файл на нужной строке.

Аналогия: цепочка передачи на курсе

Студент спрашивает куратора, куратор — преподавателя, преподаватель — методиста, и в конце выясняется, что в расписании ошибка. Стек — это запись, кто кого спросил. Чтобы найти, откуда ошибка, идём по цепочке снизу вверх, но смотрим на верхний вызов: именно он «не смог».

Стек в коде: свойство stack · Ctrl + Enter
function inner() {
  throw new Error("сломалось внутри");
}
function outer() {
  inner();
}

try {
  outer();
} catch (e) {
  const first = e.stack.split("\n")[0];   // первая строка стека — «Тип: сообщение»
  console.log(first);
  console.log("в стеке есть outer:", e.stack.includes("outer"));
}

DevTools: инструменты разработчика

Откройте DevTools на любой странице: F12 или Ctrl+Shift+I (на Mac — Cmd+Option+I). Главные вкладки для отладки:

Console — консоль

Показывает ошибки, предупреждения и ваши console.log. Можно набирать JavaScript прямо в консоли и сразу получать результат. Полезные методы:

МетодЗачем
console.log(x)Просто вывести значение. Трюк: console.log({ price, qty }) выведет имена вместе со значениями
console.table(array)В DevTools покажет массив объектов как таблицу со столбцами. (На этой странице вывод текстовый: таблицу вы увидите только в настоящем браузере)
console.trace()Печатает стек: «кто вызвал эту функцию?» — удобно, когда не понятно, откуда пришёл вызов
console.error() / console.warn()Красное сообщение / жёлтое предупреждение — их легко отфильтровать
console.group() / console.groupEnd()Сворачиваемая группа сообщений
console.time("t") / console.timeEnd("t")Замер времени между двумя точками
Подписанный вывод · Ctrl + Enter
const price = 20;
const qty = 3;

console.log(price, qty);           // 20 3 — что есть что?
console.log({ price, qty });       // { price: 20, qty: 3 } — понятно сразу
console.log("итого:", price * qty);

Sources — пошаговая отладка и breakpoints

Вместо десятка console.log можно остановить программу на нужной строке и осмотреться. Откройте вкладку Sources, выберите файл и кликните по номеру строки слева: появится синяя метка — breakpoint. Теперь выполните действие на странице (например, нажмите кнопку): выполнение замрёт перед этой строкой.

  1. Осмотритесь. Справа в панели Scope видны значения всех переменных на этот момент. В Watch можно добавить любое выражение, например items.length. В Call Stack — тот самый стек вызовов.
  2. Идите по шагам. F10 (Step over) — выполнить строку и перейти к следующей. F11 (Step into) — зайти внутрь вызываемой функции. Shift+F11 (Step out) — выйти из функции. F8 — продолжить до следующего breakpoint.
  3. Сравните ожидание с реальностью. На какой строке значение стало не таким, как вы думали? Это и есть место бага.

Удобные вариации: условный breakpoint (правый клик по номеру строки → Add conditional breakpoint, например i === 3) и Pause on exceptions (кнопка с паузой): DevTools сам остановится в момент, когда код бросит ошибку.

Оператор debugger

Если не хочется искать нужную строку в DevTools, поставьте в коде оператор debugger;. Когда DevTools открыты, выполнение остановится именно здесь, как на breakpoint. Если DevTools закрыты, оператор ничего не делает.

debugger в коде
function calcTotal(items) {
  let sum = 0;
  for (const item of items) {
    debugger;               // здесь программа остановится, если открыт DevTools
    sum += item.price;
  }
  return sum;
}
Не оставляйте debugger и лишние console.log в коде

Перед коммитом и pull request удалите их. Ревьюер увидит такой мусор в diff — и вернёт PR на доработку.

Network — сеть

Вкладка Network показывает каждый запрос страницы: HTML, картинки, fetch к API. Включите её до действия на странице (или обновите страницу). Для запросов fetch нажмите фильтр Fetch/XHR.

  • Status: 200 — успех, 404 — адрес не найден, 500 — сломался сервер. Красная строка — запрос не удался (например, нет интернета).
  • Кликните по запросу: Headers (адрес, метод, заголовки), Payload (что отправили), Preview / Response (что ответил сервер), Timing (сколько ждали).
  • Типичный вопрос «почему на странице пусто?»: смотрим в Network — пришёл ли ответ, какой статус, нет ли ошибки в теле ответа.

Метод отладки: шесть шагов

  1. Воспроизведите баг: что именно нажимать и вводить, чтобы он повторился каждый раз.
  2. Сузьте: уберите всё лишнее, пока не останется минимальный код с багом.
  3. Прочитайте сообщение об ошибке и стек. Часто ответ уже написан там.
  4. Выдвиньте гипотезу: «значение стало undefined здесь, потому что…».
  5. Проверьте гипотезу одной проверкой: console.log или breakpoint. Одной, а не пятью сразу.
  6. Исправьте и перепроверьте: баг ушёл, остальное не сломалось? Напишите в коммите, что было причиной.

Практика: закрепляем

ЗАДАЧА 7.1

Читаем стек вызовов

средняя

Перед вами текст стека ошибки (как в красной строке консоли). Допишите parseFrame(line): из строки вида at calcTotal (cart.js:12:23) достаньте имя функции, файл и номер строки. Вернуть нужно объект { fn, file, line }. Остальное уже готово: код отбирает строки, начинающиеся с at, и печатает итог.

Можно редактировать · Ctrl + Enter
const stack = `TypeError: Cannot read properties of undefined (reading 'price')
    at calcTotal (cart.js:12:23)
    at renderCart (ui.js:48:15)
    at HTMLButtonElement.<anonymous> (main.js:7:3)`;

function parseFrame(line) {
  // регулярка: at (.+?) \((.+?):(\d+):(\d+)\)
  return { fn: "?", file: "?", line: 0 };
}

const frames = stack.split("\n").filter((l) => l.trim().startsWith("at ")).map(parseFrame);
console.log("ошибка в:", frames[0].fn, frames[0].file, frames[0].line);
console.log("глубина стека:", frames.length);
console.log("всё началось в файле:", frames[frames.length - 1].file);
Ожидаемый результат
ошибка в: calcTotal cart.js 12
глубина стека: 3
всё началось в файле: main.js
Подсказка 1

Регулярка в скобках-группах: /at (.+?) \((.+?):(\d+):(\d+)\)/. Группы 1, 2, 3 — функция, файл, строка. Скобки вокруг места в стеке нужно экранировать: \( и \).

Подсказка 2

const m = line.match(…); return { fn: m[1], file: m[2], line: Number(m[3]) };

ЗАДАЧА 7.2

Лог вместо breakpoint

лёгкая

Без DevTools «посмотреть внутрь» цикла можно через console.log. Добавьте в цикл одну строку лога так, чтобы после обработки каждого числа выводилось n=…, sum=… (значения на этот момент). Итоговое значение функция печатает сама.

Можно редактировать · Ctrl + Enter
function sumPositive(numbers) {
  let sum = 0;
  for (const n of numbers) {
    if (n > 0) sum += n;
    // сюда добавьте console.log
  }
  return sum;
}

console.log(sumPositive([5, -3, 10, 0, 7]));
Ожидаемый результат
n=5, sum=5
n=-3, sum=5
n=10, sum=15
n=0, sum=15
n=7, sum=22
22
Подсказка 1

Лог ставим после строки с if, но внутри цикла: так он увидит уже обновлённую сумму.

Подсказка 2

console.log(`n=${n}, sum=${sum}`);

Читать подробнее: Chrome DevTools — отладка JavaScript, learn.javascript.ru — отладка в браузере.

08 Найди и почини баг

Большая часть багов — это небольшой набор «классических» ошибок. Если вы знаете, как они выглядят, искать станет в разы быстрее. Вот карта:

Тип багаКак выглядитГде искать
Off-by-one (ошибка на единицу)NaN или undefined в конце списка; пропущен первый или последний элементГраницы циклов: < против <=, старт с 0 или 1, slice
Неверное сравнениеУсловие срабатывает «не на тех» значениях; сортировка в странном порядке= вместо ===, строки вместо чисел, > вместо >=, sort() без функции
Порядок asyncВ выводе «Promise», сообщения идут не в том порядке, данные «ещё не пришли»Пропущенный await, forEach с async, ожидание результата вне then
МутацияДанные меняются «сами по себе» в другом месте программыsort, push, splice на исходном массиве; копия, которая на самом деле — ссылка; fill([])
Область видимостиВ колбэках цикла всегда одно и то же значениеvar вместо let
Правило для задач ниже

Сначала запустите код как есть и посмотрите, что он выводит — сравните с ожидаемым результатом. Прочитайте подсказку «симптома» в условии. Выдвиньте гипотезу, исправьте одно место и запустите снова. Подсказки открывайте только после своей попытки.

Пример разбора

Вот баг и его отладка «на словах». Запустите:

Что выведется? · Ctrl + Enter
const scores = [70, 85, 90];

function average(list) {
  let sum = 0;
  for (let i = 0; i < list.length; i++) {
    sum += list[i];
  }
  return sum / list.length + 1;
}

console.log(average(scores));

Ожидали около 81.67, получили около 82.67. Гипотеза: что-то прибавляется лишнее. Проверка: console.log(sum / list.length) перед return — это 81.67. Значит, виновата последняя строка: + 1 лишняя. Видите: мы не «правили наугад», а сузили поиск проверкой.

Off-by-one и неверные сравнения

Практика: закрепляем

ЗАДАЧА 8.1

Баг: лишний шаг цикла

лёгкая

Корзина должна вернуть сумму 445, а выводится что-то странное. Найдите причину (подсказка в консоли: у какого элемента нет цены?) и исправьте одну строку.

Можно редактировать · Ctrl + Enter
const prices = [120, 80, 45, 200];

function total(list) {
  let sum = 0;
  for (let i = 0; i <= list.length; i++) {
    sum += list[i];
  }
  return sum;
}

console.log(total(prices));
Ожидаемый результат
445
Подсказка 1

В массиве из 4 элементов индексы идут от 0 до 3. На последнем витке цикла i равен 4, а list[4] — это undefined. Сложение числа с undefined даёт NaN.

Подсказка 2

Замените <= на <. Правило: «до длины, не включая её».

ЗАДАЧА 8.2

Баг: страницы не с той позиции

средняя

Список студентов курса показывается по 3 человека на странице. Первая страница должна начинаться с «Алишер», но выводится не то. Исправьте getPage, чтобы страницы 1, 2, 3 выводили нужные группы.

Можно редактировать · Ctrl + Enter
const students = ["Алишер", "Бахтиёр", "Дилшод", "Зарина", "Мадина", "Нигина", "Рустам"];

function getPage(list, page, size) {
  const start = page * size;
  return list.slice(start, start + size);
}

console.log(getPage(students, 1, 3).join(", "));
console.log(getPage(students, 2, 3).join(", "));
console.log(getPage(students, 3, 3).join(", "));
Ожидаемый результат
Алишер, Бахтиёр, Дилшод
Зарина, Мадина, Нигина
Рустам
Подсказка 1

Люди считают страницы с 1, а массив — с 0. Первая страница начинается с индекса 0, вторая — с индекса 3.

Подсказка 2

Начало страницы: (page - 1) * size.

ЗАДАЧА 8.3

Баг: топ-3 по баллам

лёгкая

Функция должна вернуть три лучших балла по убыванию: 100, 85, 42. Но сортировка выдаёт странный порядок. Почему sort() без аргументов ошибается с числами? Исправьте.

Можно редактировать · Ctrl + Enter
function top3(scores) {
  const sorted = [...scores].sort();
  return sorted.reverse().slice(0, 3);
}

console.log(top3([85, 9, 100, 42, 7]).join(", "));
Ожидаемый результат
100, 85, 42
Подсказка 1

Без функции сравнения sort превращает числа в строки и сортирует по буквам: «100» идёт раньше «42», а «9» — позже «85».

Подсказка 2

Передайте (a, b) => b - a — так получится порядок по убыванию, и reverse() уже не нужен.

ЗАДАЧА 8.4

Баг: строки вместо чисел

средняя

Значения из полей формы всегда приходят строками. cheaper("9", "100") должна вернуть 9, а cheaper("45", "5") — 5. В label тоже ошибка: цена 0 должна печататься как «бесплатно». Найдите два бага.

Можно редактировать · Ctrl + Enter
function cheaper(a, b) {
  return a < b ? a : b;
}

function label(price) {
  if (price = 0) return "бесплатно";
  return price + " сомони";
}

console.log(cheaper("9", "100"));
console.log(cheaper("45", "5"));
console.log(label(35));
console.log(label(0));
Ожидаемый результат
9
5
35 сомони
бесплатно
Подсказка 1

Две строки сравниваются «по алфавиту»: "9" < "100" ложно, потому что «9» идёт после «1». Преобразуйте значения: Number(a) < Number(b).

Подсказка 2

В label один знак = — это присваивание: цена становится 0 при каждом вызове. Нужно сравнение ===.

Порядок асинхронного кода

ЗАДАЧА 8.5

Баг: forEach и await

средняя

Три блюда готовятся по очереди, и «Все блюда готовы!» должно появиться после трёх строк «… готов». Сейчас итоговая строка выводится первой. Исправьте код: forEach не умеет ждать async-колбэки. Замените его цикл на тот, который умеет.

Можно редактировать · Ctrl + Enter
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function cook(dish) {
  await delay(10);
  return `${dish} готов`;
}

const dishes = ["плов", "самбуса", "лагман"];

async function main() {
  dishes.forEach(async (d) => {
    console.log(await cook(d));
  });
  console.log("Все блюда готовы!");
}

main();
Ожидаемый результат
плов готов
самбуса готов
лагман готов
Все блюда готовы!
Подсказка 1

forEach запускает колбэки и не ждёт их результата, поэтому main сразу идёт дальше. Цикл for…of внутри async-функции ждёт каждый await.

Подсказка 2

for (const d of dishes) { console.log(await cook(d)); }

ЗАДАЧА 8.6

Баг: забыли await

средняя

Должно вывестись Итого: 80 (30 + 50). Вместо этого получается текст с «Promise». Функция getPrice асинхронная. Исправьте showTotal.

Можно редактировать · Ctrl + Enter
const getPrice = (id) => new Promise((resolve) => setTimeout(() => resolve(id * 10), 20));

async function showTotal() {
  const a = getPrice(3);
  const b = getPrice(5);
  console.log("Итого:", a + b);
}

showTotal();
Ожидаемый результат
Итого: 80
Подсказка 1

Без await в переменной лежит сам промис, а не число. Сложение двух промисов превращает их в строки.

Подсказка 2

Поставьте await перед каждым getPrice(...). Или используйте Promise.all, если хотите получить обе цены одновременно.

ЗАДАЧА 8.7

Баг: таймеры в цикле

лёгкая

Три таймера должны напечатать таймер 1, таймер 2, таймер 3. Но они печатают одно и то же число. Исправьте объявление переменной цикла — менять нужно одно слово.

Можно редактировать · Ctrl + Enter
for (var i = 1; i <= 3; i++) {
  setTimeout(() => console.log("таймер", i), i * 10);
}
Ожидаемый результат
таймер 1
таймер 2
таймер 3
Подсказка 1

Переменная var одна на весь цикл, и к моменту срабатывания таймеров цикл уже закончился (i стал 4).

Подсказка 2

С let на каждом витке создаётся своя переменная i.

Мутация и общие ссылки

ЗАДАЧА 8.8

Баг: sort портит корзину

средняя

Функция cheapest должна только найти самый дешёвый товар, а порядок в корзине менять нельзя: после вызова в корзине по-прежнему Чай, Нон, Плов. Сейчас порядок меняется. Исправьте одну строку.

Можно редактировать · Ctrl + Enter
const cart = [
  { name: "Чай", price: 15 },
  { name: "Нон", price: 4 },
  { name: "Плов", price: 35 },
];

function cheapest(items) {
  return items.sort((a, b) => a.price - b.price)[0];
}

console.log(cheapest(cart).name);
console.log(cart.map((i) => i.name).join(", "));
Ожидаемый результат
Нон
Чай, Нон, Плов
Подсказка 1

sort меняет сам массив (мутирует), а не возвращает новый. В items лежит ссылка на тот же массив, что и cart.

Подсказка 2

Отсортируйте копию: [...items].sort(…).

ЗАДАЧА 8.9

Баг: общая ссылка

сложная

Два бага с одной причиной — несколько «переменных» смотрят на один и тот же массив. (1) Сетка из трёх строк: после добавления «A1» в первую и «C1» в третью длины должны быть 1,0,1. (2) Копия состояния copy не должна влиять на state: теги в нём должны остаться js. Исправьте обе части.

Можно редактировать · Ctrl + Enter
const grid = new Array(3).fill([]);
grid[0].push("A1");
grid[2].push("C1");
console.log(grid.map((row) => row.length).join(","));

const state = { user: "Али", tags: ["js"] };
const copy = { ...state };
copy.tags.push("git");
console.log(state.tags.join(","));
Ожидаемый результат
1,0,1
js
Подсказка 1

new Array(3).fill([]) кладёт в три ячейки один и тот же массив. Нужно создать три разных: Array.from({ length: 3 }, () => []) — функция вызывается для каждой ячейки заново.

Подсказка 2

Spread { ...state } копирует только верхний уровень, а tags остаётся общим. Скопируйте и его: { ...state, tags: [...state.tags] }.

Читать подробнее: learn.javascript.ru — сортировка sort(fn), MDN — Array.prototype.sort.

09 ИИ: коммиты, описание PR и вопрос про баг

ИИ-блок урока

ИИ хорошо делает две вещи: формулирует коммит по готовому diff и подсказывает гипотезы про баг. Но ответственность на вас: diff вы прочитали сами, а гипотезы проверяете запуском, а не верите на слово. Правила курса: сначала 15 минут пробуйте сами; не вставляйте код, который не можете объяснить построчно.

1. Коммит и описание PR по diff

Скопируйте вывод git diff --staged и попросите ИИ сформулировать сообщение. Без контекста он напишет «update files»:

придумай коммит
Вот diff: [вставьте вывод git diff --staged]. Напиши коммит-сообщение в стиле Conventional Commits (тип: тема, до 50 символов, английский, повелительное наклонение, без точки) и краткое описание PR: что сделано и зачем. Формат ответа: сначала одна строка коммита, потом PR из трёх разделов «Что сделано», «Зачем», «Как проверить». Не придумывай то, чего нет в diff.

Что проверить в ответе:

  • Тип соответствует правде: feat или fix, а не «на всякий случай».
  • В тексте нет того, чего нет в diff (ИИ любит добавлять «улучшил производительность», когда вы её не трогали).
  • Сообщение конкретное: пропустите его через проверки из задач 5.3 и 9.2.
  • Если в diff две разные мысли, лучше разбить на два коммита, а не просить ИИ «объединить красиво».

2. Баг: вместо «не работает» — минимальный воспроизводимый пример

у меня не работает корзина, исправь
Баг: ожидаю [что], получаю [что]. Минимальный код: [код]. Ошибка в консоли: [текст]. Дай 3 гипотезы причины по вероятности.

Минимальный воспроизводимый пример (minimal reproducible example, MRE) — самый короткий код, в котором баг всё ещё виден. Как его получить:

  1. Скопируйте проблемное место в отдельный файл или в консоль на этой странице.
  2. Удаляйте всё лишнее по кусочку: стили, соседние функции, лишние поля. После каждого шага проверяйте, что баг на месте.
  3. Подставьте простые данные вместо запросов к серверу: массив из 2–3 элементов.
  4. Запишите ожидаемый и фактический результат, а также текст ошибки дословно.

Бонус: пока вы упрощали код, вы часто сами находите баг. Это нормально и очень ценно. Метод называется «утёнок»: объяснить проблему вслух настолько ясно, что ответ находится сам.

Как не надо работать с гипотезами

ИИ дал три гипотезы. Проверяйте по очереди, от самой вероятной, и после каждой — запуск. Если вставить сразу три «исправления», вы не узнаете, какое сработало, и не научитесь замечать этот баг в следующий раз. Также ИИ иногда уверенно советует несуществующие методы: незнакомое название проверяйте в MDN.

В шпаргалку промптов добавьте две строки этого урока — «Коммиты и pull request» и «Баг вместо „не работает“». Потренируем оба навыка кодом.

Практика: закрепляем

ЗАДАЧА 9.1

Промпт с минимальным примером

средняя

Допишите bugPrompt(bug): она собирает из описания бага готовый промпт для ИИ по формату курса — четыре строки (код идёт со второй строки). Если ошибки в консоли нет (bug.error не задана), в строке ошибки пишем нет.

Можно редактировать · Ctrl + Enter
function bugPrompt(bug) {
  // верните строку из строк:
  // Баг: ожидаю «…», получаю «…».
  // Минимальный код:
  // <код>
  // Ошибка в консоли: …
  // Дай 3 гипотезы причины по вероятности.
}

console.log(bugPrompt({
  expected: "45",
  actual: "NaN",
  code: "const prices = [20, 25];\nlet sum = 0;\nfor (let i = 0; i <= prices.length; i++) sum += prices[i];\nconsole.log(sum);",
}));
console.log("---");
console.log(bugPrompt({ expected: "Привет, Али", actual: "Привет, undefined", code: "greet({ nam: 'Али' })", error: "ошибки нет" }));
Ожидаемый результат
Баг: ожидаю «45», получаю «NaN».
Минимальный код:
const prices = [20, 25];
let sum = 0;
for (let i = 0; i <= prices.length; i++) sum += prices[i];
console.log(sum);
Ошибка в консоли: нет
Дай 3 гипотезы причины по вероятности.
---
Баг: ожидаю «Привет, Али», получаю «Привет, undefined».
Минимальный код:
greet({ nam: 'Али' })
Ошибка в консоли: ошибки нет
Дай 3 гипотезы причины по вероятности.
Подсказка 1

Соберите массив из пяти строк (вторая — «Минимальный код:», третья — сам код) и объедините через join("\n").

Подсказка 2

Для необязательного поля подойдёт оператор ??: bug.error ?? "нет" вернёт «нет», если ошибка не задана.

ЗАДАЧА 9.2

Расплывчатые сообщения от ИИ

лёгкая

ИИ иногда предлагает коммиты вроде «update» или «fix: bug». Допишите isVague(msg): сообщение расплывчато, если после типа и двоеточия (если они есть) в теме меньше двух слов или есть слово из списка generic.

Можно редактировать · Ctrl + Enter
const generic = ["stuff", "things", "changes", "bug", "bugs", "code", "files"];

function isVague(msg) {
  // 1) уберите тип: msg.replace(/^\w+(\([^)]*\))?:\s*/, "")
  // 2) посчитайте слова и проверьте generic
  return false;
}

const ai = [
  "feat: add order filters",
  "update",
  "fix: bug",
  "chore: bump deps",
  "fix: update stuff",
  "docs: describe setup steps",
];
ai.forEach((m) => console.log(`${m} -> ${isVague(m) ? "vague" : "ok"}`));
Ожидаемый результат
feat: add order filters -> ok
update -> vague
fix: bug -> vague
chore: bump deps -> ok
fix: update stuff -> vague
docs: describe setup steps -> ok
Подсказка 1

Тему получаем так: msg.replace(/^\w+(\([^)]*\))?:\s*/, ""). Для «update» (без двоеточия) эта замена ничего не изменит — и это правильно.

Подсказка 2

const words = subject.split(/\s+/).filter(Boolean); return words.length < 2 || words.some((w) => generic.includes(w));

10 Практика на уроке: работа в парах

Цель: пройти полный цикл командной работы — ветка → PR → ревью → merge — и найти три подсаженных бага в проекте напарника. Работаем в парах: студент А и студент Б. Ориентир по времени на 90 минут: подготовка 10, цикл с PR 35, конфликт 10, охота за багами 25, разбор 10.

Часть 1. Репозиторий и роли (10 минут)

  1. Студент А создаёт на GitHub репозиторий pair-shop (с README) и в настройках Settings → Collaborators приглашает Б. Б принимает приглашение в почте.
  2. Оба клонируют репозиторий:
    Терминал
    git clone https://github.com/ЛОГИН_А/pair-shop.git
    cd pair-shop
  3. Договоритесь, что вы делаете: А — функцию calcTotal и discount, Б — отображение корзины. Каждый работает в своём файле.

Часть 2. Ветка, коммиты, PR (35 минут)

  1. Каждый создаёт свою ветку: А — feature/calc-total, Б — feature/cart-view.
  2. Сделайте минимум три коммита в ветке, каждый — одна мысль, сообщения по Conventional Commits:
    Терминал
    git add cart.js
    git commit -m "feat: add calcTotal"
    git add cart.js
    git commit -m "feat: add 10 percent discount"
    git add README.md
    git commit -m "docs: describe cart module"
  3. Отправьте ветку и откройте pull request на GitHub. В описании — три раздела: «Что сделано», «Зачем», «Как проверить». Ревьюер — напарник.
    Терминал
    git push -u origin feature/calc-total
  4. Ревью: прочитайте PR напарника по чек-листу из раздела 3. Оставьте минимум два комментария к конкретным строкам: один с вопросом, один с предложением. Запустите код ветки.
  5. Автор отвечает на комментарии и правит код новыми коммитами в ту же ветку. Ревьюер нажимает Approve.
  6. Автор нажимает Merge pull request. Затем оба обновляют main:
    Терминал
    git switch main
    git pull
    git branch -d feature/calc-total

Часть 3. Конфликт нарочно (10 минут)

  1. Оба создают новые ветки от свежего main и меняют одну и ту же строку в README (например, заголовок) — по-разному.
  2. А вливает свой PR первым. Б делает git pull origin main в своей ветке — появляется конфликт.
  3. Б решает конфликт вместе с А (договариваются об итоговом тексте), делает git add, git commit, git push. PR Б обновился и больше не конфликтует.

Часть 4. Три подсаженных бага (25 минут)

Каждый тайком вносит в свой проект три бага разных типов (из карты в разделе 8) и пушит в новую ветку bugs/названия. Напарник чинит их, пользуясь методом из раздела 7: воспроизвести, прочитать ошибку, поставить breakpoint или лог, сделать исправление отдельным коммитом fix: … с причиной. Тренировочная версия такого проекта — задача ниже: прочитайте её как ревью чужого кода.

Практика: закрепляем

ЗАДАЧА 10.1

Три бага в чужом проекте

сложная

Напарник написал корзину магазина MakeFirstStep и просит о ревью. Скидка 10% действует от 500 сомони включительно, доставка стоит 15 сомони и добавляется только к копии корзины. В коде спрятаны три бага разных типов. Найдите их, исправьте и в комментарии над каждой правкой напишите причину одной строкой. Ожидаемые числа ниже — это правильные ответы.

Можно редактировать · Ctrl + Enter
const products = [
  { id: 1, name: "Курс Frontend", price: 450 },
  { id: 2, name: "Футболка MakeFirstStep", price: 80 },
  { id: 3, name: "Блокнот", price: 20 },
];

function calcTotal(items) {
  let sum = 0;
  for (let i = 1; i < items.length; i++) {
    sum += items[i].price;
  }
  return sum;
}

function priceWithDiscount(items) {
  const total = calcTotal(items);
  return total > 500 ? total * 0.9 : total;
}

function withDelivery(items) {
  items.push({ id: 0, name: "Доставка", price: 15 });
  return items;
}

const cart = [products[0], products[1], products[2]];
const cart500 = [products[0], { id: 9, name: "Стикеры", price: 50 }];

console.log(calcTotal(cart));
console.log(priceWithDiscount(cart));
console.log(priceWithDiscount(cart500));
console.log(calcTotal(withDelivery(cart)));
console.log(cart.length);
Ожидаемый результат
550
495
450
565
3
Подсказка 1

Три бага: (1) цикл начинается с i = 1 и пропускает первый товар; (2) в priceWithDiscount стоит > вместо >= — ровно 500 не получает скидку; (3) push меняет исходную корзину.

Подсказка 2

Проверьте себя числами: сумма корзины 550, со скидкой 495, корзина на ровно 500 — 450, с доставкой 565, после этого в исходной корзине по-прежнему 3 товара.

Чек-лист пары на выходе

В репозитории: три и более коммита с осмысленными сообщениями, один влитый PR с описанием и ревью-комментариями, один разрешённый конфликт, три fix:-коммита с описанием причины. Если чего-то нет, доделайте дома: это входит в домашнее задание.

11 Домашнее задание

Оформите репозиторий так, чтобы его не стыдно было показать работодателю, и найдите три бага с разбором причины. Можно взять проект одного из прошлых уроков (например, мини-проект №1) или проект из пары.

Часть 1. Репозиторий

  1. Репозиторий на GitHub публичный, у него осмысленное имя и описание.
  2. В истории минимум 10 осмысленных коммитов по Conventional Commits: каждый — одна мысль, ни одного «update», «fix», «final». Проверьте историю командой git log --oneline.
  3. Есть минимум одна ветка кроме main и влитый pull request с описанием (что сделано, зачем, как проверить). Если работаете в одиночку, откройте PR из своей ветки в main и сами посмотрите diff как ревьюер.
  4. В корне есть .gitignore (минимум: node_modules/, .env, .DS_Store, *.log) и в репозитории нет мусорных файлов.
  5. В корне есть README.md по шаблону ниже.
README.md — шаблон
# Название проекта

Одно предложение: что это и для кого.

## Что умеет
- функция 1
- функция 2

## Как запустить
1. Склонируйте репозиторий
2. Откройте index.html в браузере (или npm install && npm run dev)

## Скриншот
![скриншот](screenshot.png)

## Что я использовал
HTML, CSS, JavaScript, Git/GitHub

## Автор
Имя, группа курса

Часть 2. Три бага

  1. Найдите в своём или в чужом проекте три реальных бага разных типов: например, off-by-one, неверное сравнение, мутация, ошибка порядка асинхронного кода. Если не нашлось — внесите их в копию проекта сами и попросите знакомого найти.
  2. Для каждого бага сделайте отдельный коммит fix: … с исправлением.
  3. Создайте в репозитории файл BUGS.md и опишите каждый баг по шаблону ниже.
  4. Один из трёх разборов сопроводите промптом к ИИ в формате «минимальный воспроизводимый пример» и вставьте в файл ответ ИИ вместе со своей оценкой: что подтвердилось запуском, а что оказалось неверным.
BUGS.md — шаблон
## Баг 1: итоговая сумма корзины меньше правильной

- **Симптом:** ожидал 550, получил 530.
- **Как воспроизвести:** добавить три товара и посмотреть сумму.
- **Причина:** цикл начинался с i = 1 и пропускал первый товар (off-by-one).
- **Как нашёл:** поставил breakpoint в calcTotal, увидел, что цикл идёт с индекса 1.
- **Исправление:** коммит `a1b2c3d` — `fix: count first item in total`.
- **Как не допустить снова:** проверять границы цикла на массиве из 1 и 0 элементов.

Что сдавать

  • Ссылку на публичный репозиторий GitHub.
  • Ссылку на влитый pull request.
  • В репозитории: README.md, .gitignore, BUGS.md.

Как оцениваем (0–10)

ЧтоБаллы
10+ осмысленных коммитов по формату, нет «update» и «fix» без пояснения3
Ветка, pull request с описанием, влит в main2
.gitignore и README.md оформлены по шаблону, нет мусорных файлов2
Три бага разных типов с отдельными fix:-коммитами2
В BUGS.md понятны причина и как нашли; один разбор с ИИ и проверкой запуском1
Перед сдачей

Откройте репозиторий в приватном окне браузера: всё ли видно без входа? Проверьте git log --oneline: прочитайте сообщения как незнакомец. Если ИИ помогал с кодом или разбором, вы должны уметь объяснить каждую строку на защите.

✓ Шпаргалка урока

  • Три зоны: рабочая папка → индекс (git add) → репозиторий (git commit); GitHub — копия в сети (git push / git pull). Перед add всегда git status.
  • Ветка — закладка на коммит. git switch -c feature/имя, работа, push. В main напрямую не пишем.
  • Цикл в паре: ветка → коммиты → push → pull request → ревью (комментарии, approve) → merge → удалить ветку.
  • Конфликт: маркеры <<<<<<< / ======= / >>>>>>>. Выбрать итог, удалить маркеры, запустить, add + commit. Запутались — git merge --abort.
  • .gitignore: node_modules/, dist/, .env, *.log, .DS_Store. Секрет, попавший в историю, считается утёкшим: меняем ключ.
  • Коммит: тип(область): что сделано — feat, fix, docs, style, refactor, test, chore. До 50 символов, строчные, без точки, одна мысль.
  • try / catch / finally: catch получает ошибку, finally выполняется всегда. throw new Error("текст") бросает свою ошибку. Пустой catch — плохо; чужие ошибки пробрасываем дальше.
  • Типы ошибок: TypeError (не то значение, null.x), ReferenceError (нет переменной), SyntaxError (плохой синтаксис / JSON), RangeError (вне диапазона).
  • Стек: читаем сверху вниз: где упало, кто вызвал. DevTools: Console (log, table, trace), Sources (breakpoint по номеру строки, F10 / F11 / F8, Watch, Scope), debugger;, Network (статус, ответ, время).
  • Классические баги: off-by-one (< / <=, индексы с 0), сравнение (===, строки против чисел, sort(fn)), async (await, for…of вместо forEach), мутация ([...arr], Array.from), var в цикле.
  • Метод отладки: воспроизвести → сузить → прочитать ошибку → гипотеза → одна проверка → исправить и перепроверить.
  • С ИИ: коммит и PR — по вашему diff, текст проверяем. Баг — минимальный пример + ожидание/факт + дословная ошибка + «3 гипотезы»; проверяем по очереди запуском.