JavaScript DOM — 30 приёмов

Приветствую! Здесь вы наверняка найдете, что ищете. Примеры в лаборатории рассчитаны на то, что мы разбираем что-то конкретное.

Текущая статья посвящена готовым примеры DOM в браузере с разбором.

Поэтому за теорией по текущей теме вам — в энциклопедию. Если ещё не погружались, то маршрут прост:

  1. Основы
  2. Система и сеть
  3. Данные и разметка
  4. Код и разработка
  5. Языки
  6. Искусственный интеллект
  7. Проект
  8. Инфраструктура и безопасность
  9. Спин-офф

Обязательно пройдитесь.

А теперь приступим к нашему предмету.

Теория и соседние материалы

Перед практикой откройте Работа с HTML в JavaScript — дерево узлов, шпаргалка API, связь с CSSOM.

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

Каркас страницы — HTML-страницы целиком и HTML + CSS — макеты.

Запросы к API — Fetch / axios.

Дальше по стеку — React и Vue и Svelte .


Основы DOM в браузере

Симулятор выше показывает, как HTML-разметка превращается в дерево DOM и в каком порядке выполняется скрипт.

Вставьте любой пример из статьи в редактор — предпросмотр обновится через пару секунд.


Базовые термины

Термин Простыми словами
DOM Дерево объектов страницы: теги, текст, атрибуты
document Корень документа, точка входа для поиска и создания узлов
Element Узел-тег (div, button, …)
NodeList Список узлов из querySelectorAll (не «живой», как старые коллекции)
addEventListener Подписка на клик, ввод, клавиатуру без атрибута onclick в HTML
classList Переключение CSS-классов без ручной склейки строки className
Делегирование Один обработчик на родителе вместо сотни на каждой кнопке списка

Как запустить пример за 30 секунд

  1. Скопируйте весь блок от <!DOCTYPE html> до </html>.
  2. Сохраните как dom-demo.html и откройте двойным щелчком или перетащите в браузер.
  3. Откройте Инструменты разработчика (F12) → вкладка Console, если в примере есть console.log.
  4. Меняйте селекторы и классы по одному — сразу видно, что сломалось.
Где Плюсы
Локальный .html Офлайн, урок, портфолио
HTMLPlayground на этой странице Быстрая проверка без файла
CodePen / JSFiddle Поделиться ссылкой с одногруппниками

Указатель — 30 приёмов

Приём Якорь
1 querySelector #1-queryselector
2 querySelectorAll #2-queryselectorall
3 getElementById #3-getelementbyid
4 closest и matches #4-closest-i-matches
5 children #5-children
6 textContent #6-textcontent
7 classList #7-classlist
8 dataset #8-dataset
9 setAttribute #9-setattribute
10 hidden и aria-expanded #10-hidden
11 addEventListener #11-addeventlistener
12 DOMContentLoaded #12-domcontentloaded
13 Делегирование #13-delegirovanie
14 preventDefault #14-preventdefault
15 stopPropagation #15-stoppropagation
16 input и value #16-input
17 change #17-change
18 FormData #18-formdata
19 checked / disabled #19-checked
20 reportValidity #20-reportvalidity
21 createElement + append #21-createelement
22 remove / replaceChildren #22-remove
23 insertAdjacentElement #23-insertadjacent
24 DocumentFragment #24-documentfragment
25 <template> #25-template
26 cloneNode #26-clonenode
27 CustomEvent #27-customevent
28 focus() #28-focus
29 Клавиша Escape #29-escape
30 Безопасный список (без XSS) #30-bezopasnyy-spisok

Обязательный каркас

Любой пример ниже можно собрать на этом фундаменте. Скрипт в конце <body> — разметка уже в DOM к моменту выполнения.

Разбор.

Фрагмент Смысл
lang="ru" Язык страницы для скринридеров и поисковиков
id="app" Удобная «корневая» точка для querySelector('#app')
<script> в конце body Элементы выше уже существуют — не нужен DOMContentLoaded для простых демо

Стартовые приёмы

Три мини-примера, с которых обычно начинают урок по DOM.


Приветствие по кнопке

Разбор. getElementById — самый быстрый поиск, если id уникален. textContent задаёт текст без HTML-тегов.


Счётчик кликов

<button type="button" id="plus">+1</button>
<span id="count">0</span>
<script>
  let n = 0;
  const countEl = document.getElementById('count');
  document.getElementById('plus').addEventListener('click', () => {
    n += 1;
    countEl.textContent = String(n);
  });
</script>

Разбор. Состояние (n) живёт в JavaScript; DOM только отображает число. Так же работают счётчики лайков и корзины.


Переключение класса «активен»

Разбор. Внешний вид меняется через CSS-класс, а не через десяток свойств style.* — проще сопровождать.


30 приёмов DOM

Ниже — полный набор паттернов для учебного проекта, лабораторной или первого интерактивного сайта без React.


1. Поиск — querySelector

1.1. Первый элемент по CSS-селектору

<nav class="menu">
  <a href="/" class="menu__link is-current">Главная</a>
  <a href="/blog" class="menu__link">Блог</a>
</nav>
<script>
  const current = document.querySelector('.menu__link.is-current');
  console.log(current?.href); // URL активной ссылки
</script>

Разбор.

API Когда
querySelector('.menu__link') Первое совпадение или null
?. (optional chaining) Если элемента нет — не падаем с ошибкой

2. Поиск — querySelectorAll

2.1. Все карточки и цикл

<ul class="cards">
  <li class="card">Раз</li>
  <li class="card">Два</li>
  <li class="card">Три</li>
</ul>
<script>
  document.querySelectorAll('.card').forEach((card, index) => {
    card.dataset.index = String(index);
    card.style.opacity = index === 0 ? '1' : '0.6';
  });
</script>

Разбор. NodeList поддерживает forEach. Список статический — новые узлы, добавленные позже, в него не попадут (в отличие от старых «живых» коллекций).


3. Поиск — getElementById

3.1. Уникальный элемент

const modal = document.getElementById('settings-modal');
if (modal) {
  modal.hidden = false;
}

Разбор. Один id на страницу. Быстрее произвольного селектора, когда имя известно заранее.


4. closest и matches

4.1. Клик по кнопке внутри карточки

<article class="product" data-id="42">
  <h2>Книга</h2>
  <button type="button" class="product__buy">В корзину</button>
</article>
<script>
  document.body.addEventListener('click', (e) => {
    const btn = e.target.closest('.product__buy');
    if (!btn) return;
    const card = btn.closest('.product');
    const id = card?.dataset.id;
    console.log('Добавить товар', id);
  });
</script>

Разбор.

Метод Назначение
closest('.product') Подняться от target к предку
matches('.product__buy') Проверить, что элемент сам кнопка (в делегировании)

5. Дети — children

5.1. Только элементы, без текстовых узлов

<ul id="list">
  <li>Первый</li>
  <li>Второй</li>
</ul>
<script>
  const items = document.getElementById('list').children;
  console.log(items.length); // 2
  console.log(items[0].textContent);
</script>

Разбор. children — только теги. childNodes включает пробелы и переносы строк между тегами — для обхода разметки чаще удобнее children.


6. Текст — textContent

6.1. Вывод без разбора HTML

const title = document.querySelector('#title');
title.textContent = 'Новый заголовок';

// Пользовательский ввод — только textContent:
const name = document.querySelector('#name-input').value;
document.querySelector('#greet').textContent = `Здравствуйте, ${name}`;

Разбор. textContent экранирует уголки — вставленный текст не станет тегом <script>. Для разметки от сервера нужна отдельная санитизация.


7. Классы — classList

7.1. add, remove, toggle

<button type="button" id="theme">Тёмная тема</button>
<script>
  const root = document.documentElement;
  document.getElementById('theme').addEventListener('click', () => {
    root.classList.toggle('theme-dark');
    const on = root.classList.contains('theme-dark');
    document.getElementById('theme').textContent = on ? 'Светлая тема' : 'Тёмная тема';
  });
</script>

Разбор. toggle добавляет класс, если его не было, и снимает, если был — один вызов вместо if/else.


8. Данные — dataset

8.1. data-* в JavaScript

<button type="button" class="chip" data-color="blue" data-size="m">Синий M</button>
<script>
  document.querySelector('.chip').addEventListener('click', (e) => {
    const { color, size } = e.currentTarget.dataset;
    console.log(color, size); // "blue", "m"
  });
</script>

Разбор. Атрибут data-user-id доступен как dataset.userId (camelCase).


9. Атрибуты — setAttribute

9.1. aria-* и href

const link = document.querySelector('a.download');
link.setAttribute('download', '');
link.setAttribute('aria-label', 'Скачать PDF отчёт');

Разбор. Для стандартных свойств (href, disabled) часто достаточно полей элемента: link.href = '..'. setAttribute нужен для aria-*, data-* и нестандартных имён.


10. Видимость — hidden и aria-expanded

10.1. Раскрывающийся блок

Разбор. hidden убирает блок из доступного дерева и обычно скрывает визуально. aria-expanded сообщает скринридеру, открыт ли раздел.


11. Событие — addEventListener

11.1. Клик без onclick в HTML

const saveBtn = document.querySelector('[data-action="save"]');
saveBtn.addEventListener('click', (event) => {
  event.preventDefault();
  // сохранение..
});

Разбор. Несколько обработчиков на одном элементе не затирают друг друга. &#123; once: true &#125; — сработает один раз (удобно для «принять cookie»).


12. Старт — DOMContentLoaded

12.1. Скрипт в <head>

<head>
  <script>
    document.addEventListener('DOMContentLoaded', () => {
      const app = document.querySelector('#app');
      app.textContent = 'Разметка готова';
    });
  </script>
</head>
<body>
  <div id="app"></div>
</body>

Разбор. Событие раньше load (картинки ещё грузятся). Если <script> стоит в конце body, для простых страниц хватит прямого кода без слушателя.


13. Делегирование

13.1. Список задач — одна подписка

<ul id="todos">
  <li><button type="button" data-delete>×</button> Купить хлеб</li>
  <li><button type="button" data-delete>×</button> Сдать лабу</li>
</ul>
<script>
  document.getElementById('todos').addEventListener('click', (e) => {
    const del = e.target.closest('[data-delete]');
    if (!del) return;
    del.closest('li')?.remove();
  });
</script>

Разбор. Новые <li> получают работающую кнопку «удалить» без повторного addEventListener на каждой строке.


14. preventDefault

14.1. Форма без перезагрузки страницы

<form id="subscribe">
  <input type="email" name="email" required>
  <button type="submit">Подписаться</button>
</form>
<script>
  document.getElementById('subscribe').addEventListener('submit', (e) => {
    e.preventDefault();
    const email = new FormData(e.target).get('email');
    console.log('Отправили бы на сервер:', email);
  });
</script>

Разбор. Без preventDefault браузер перезагрузит страницу методом GET/POST.form. Для SPA и учебных демо обработчик перехватывает отправку.


15. stopPropagation

15.1. Кнопка внутри кликабельной карточки

<article class="card" data-open>
  <h2>Новость</h2>
  <button type="button" class="card__fav">★</button>
</article>
<script>
  document.querySelector('.card').addEventListener('click', () => {
    console.log('Открыть новость');
  });
  document.querySelector('.card__fav').addEventListener('click', (e) => {
    e.stopPropagation();
    console.log('Только в избранное');
  });
</script>

Разбор. Всплытие останавливается — клик по «звезде» не открывает всю карточку. Для сложных UI чаще проверяют e.target в одном обработчике.


16. Поле ввода — input

16.1. Счётчик символов в реальном времени

<label>
  Комментарий
  <textarea id="comment" maxlength="200"></textarea>
</label>
<p><span id="left">200</span> символов осталось</p>
<script>
  const area = document.getElementById('comment');
  const left = document.getElementById('left');
  area.addEventListener('input', () => {
    left.textContent = String(200 - area.value.length);
  });
</script>

Разбор. input срабатывает на каждый символ. change — только после потери фокуса (для текста неудобно).


17. Выбор — change

17.1. <select> и фильтр

<select id="sort">
  <option value="name">По имени</option>
  <option value="date">По дате</option>
</select>
<script>
  document.getElementById('sort').addEventListener('change', (e) => {
    console.log('Сортировка:', e.target.value);
  });
</script>

Разбор. Для <select> и checkbox change — правильный момент: значение уже зафиксировано.


18. Форма — FormData

18.1. Сбор полей одной строкой

const form = document.querySelector('#profile');
const data = Object.fromEntries(new FormData(form));
console.log(data); // { name: "..", city: ".." }

Разбор. FormData учитывает name у полей, файлы и несколько checkbox с одним именем. Для отправки на сервер — fetch(url, &#123; method: 'POST', body: new FormData(form) &#125;).


19. Состояние — checked и disabled

19.1. «Выбрать всё»

<label><input type="checkbox" id="all"> Выбрать всё</label>
<label><input type="checkbox" class="row" value="1"> Строка 1</label>
<label><input type="checkbox" class="row" value="2"> Строка 2</label>
<script>
  const all = document.getElementById('all');
  const rows = () => document.querySelectorAll('.row');
  all.addEventListener('change', () => {
    rowsforEach((cb) => { cb.checked = all.checked; });
  });
</script>

Разбор. Свойство checked — булево. disabled = true отключает поле и убирает его из таб-навигации.


20. Валидация — reportValidity

20.1. Встроенные правила HTML5

<form id="login">
  <input type="email" name="email" required>
  <input type="password" name="password" minlength="8" required>
  <button type="submit">Войти</button>
</form>
<script>
  document.getElementById('login').addEventListener('submit', (e) => {
    if (!e.target.reportValidity()) {
      e.preventDefault();
    }
  });
</script>

Разбор. Браузер показывает подсказки у полей. Подробнее о кастомных сообщениях — Валидация форм.


21. Создание — createElement + append

21.1. Новый пункт списка

<ul id="list"></ul>
<button type="button" id="add">Добавить</button>
<script>
  document.getElementById('add').addEventListener('click', () => {
    const li = document.createElement('li');
    li.textContent = `Пункт ${Date.now()}`;
    document.getElementById('list').append(li);
  });
</script>

Разбор. append принимает несколько узлов и строк (строки превращаются в текстовые узлы). Старый appendChild — только один узел.


22. Удаление — remove и replaceChildren

22.1. Очистить контейнер

const list = document.querySelector('#list');
list.replaceChildren(); // пусто, быстрее цикла remove

// или один элемент:
document.querySelector('.toast')?.remove();

Разбор. replaceChildren() — современная замена innerHTML = '' без разбора строки как HTML.


23. Вставка — insertAdjacentElement

23.1. Баннер перед заголовком

const banner = document.createElement('p');
banner.className = 'banner';
banner.textContent = 'Акция до воскресенья';
const h1 = document.querySelector('h1');
h1.insertAdjacentElement('beforebegin', banner);

Разбор. Позиции: beforebegin, afterbegin, beforeend, afterend — относительно элемента, не только его содержимого.


24. Пакет — DocumentFragment

24.1. Сто строк — одна перерисовка

const ul = document.querySelector('#big-list');
const frag = document.createDocumentFragment();
for (let i = 1; i <= 100; i++) {
  const li = document.createElement('li');
  li.textContent = `Строка ${i}`;
  frag.append(li);
}
ul.append(frag);

Разбор. Пока узлы в fragment, они не на странице — браузер не пересчитывает layout на каждой итерации.


25. Шаблон — <template>

25.1. Клон разметки

Разбор. Содержимое <template> не отображается и не выполняет скрипты до клонирования — удобно для таблиц и карточек.


26. Копия — cloneNode

26.1. Дублировать блок настроек

const proto = document.querySelector('.field-group');
const copy = proto.cloneNode(true); // true — с потомками
copy.querySelector('input').value = '';
proto.after(copy);

Разбор. cloneNode(false) — только оболочка без детей. Обработчики событий не копируются — их вешают заново или используют делегирование.


27. Своё событие — CustomEvent

27.1. Сигнал между модулями страницы

document.addEventListener('cart:updated', (e) => {
  document.querySelector('#cart-count').textContent = e.detail.count;
});

function addToCart(count) {
  document.dispatchEvent(
    new CustomEvent('cart:updated', { detail: { count } })
  );
}

Разбор. Имена с двоеточием (cart:updated) — соглашение, не магия браузера. В React/Vue чаще state, но в ванильном учебном проекте CustomEvent нагляден.


28. Фокус — focus()

28.1. Открыть модалку — фокус в поле

<dialog id="dlg">
  <input type="text" id="dlg-input">
  <button type="button" id="dlg-close">Закрыть</button>
</dialog>
<button type="button" id="open">Открыть</button>
<script>
  const dlg = document.getElementById('dlg');
  document.getElementById('open').addEventListener('click', () => {
    dlg.showModal();
    document.getElementById('dlg-input').focus();
  });
  document.getElementById('dlg-close').addEventListener('click', () => dlg.close());
</script>

Разбор. focus() переводит клавиатуру в поле. Для доступности после закрытия диалога верните фокус на кнопку «Открыть».


29. Клавиатура — Escape

29.1. Закрыть панель по Esc

document.addEventListener('keydown', (e) => {
  if (e.key !== 'Escape') return;
  document.querySelector('.drawer.open')?.classList.remove('open');
});

Разбор. Проверяйте e.key === 'Escape', а не устаревший keyCode. Для модалок <dialog> браузер часто закрывает их сам — уточняйте поведение в целевых браузерах.


30. Безопасный список — без XSS

30.1. Комментарии пользователя

Разбор.

Способ Риск при пользовательском вводе
textContent Низкий — текст не выполняется как HTML
innerHTML = userInput Высокий — возможен XSS
innerHTML с серверным HTML Нужна санитизация (DOMPurify и т.п.)

Типичные ошибки

  • Поиск элемента до появления разметки — null и ошибка при обращении к свойству. Решение: скрипт в конце body или DOMContentLoaded.
  • getElementsByClassName в цикле с удалением — «живая» коллекция смещается. Решение: querySelectorAll или идти с конца.
  • Сотня addEventListener на динамический список — утечки и лишняя память. Решение: делегирование.
  • innerHTML += в цикле — медленно и опасно с чужим текстом. Решение: DocumentFragment или textContent.

Чек-лист перед сдачей лабораторной

  • Уникальные id, осмысленные классы и data-* вместо «магических» номеров в разметке.
  • Обработчики сняты или делегированы, если узлы часто удаляются (для долгоживущих страниц).
  • Формы с required / type="email" и reportValidity на submit.
  • Пользовательский текст в DOM через textContent, не через сырой innerHTML.
  • Кнопки с type="button" внутри форм, если не должны отправлять форму.

Куда двигаться дальше

Задача Материал
События, drag-and-drop События в браузере
Модалки, табы, аккордеон Виджеты на ванильном JS
MutationObserver, lazy load Наблюдатели DOM
Вопросы на собеседование 200 вопросов по JavaScript
Рисование на холсте p5.js — фигуры
HTTP из формы и fetch Fetch / axios
React-компоненты React — рецепты
Vue, Svelte, реактивный UI Vue и Svelte — компоненты
Анимация без тяжёлого JS CSS-анимации