JavaScript DOM — 30 приёмов
Приветствую! Здесь вы наверняка найдете, что ищете. Примеры в лаборатории рассчитаны на то, что мы разбираем что-то конкретное.
Текущая статья посвящена готовым примеры DOM в браузере с разбором.
Поэтому за теорией по текущей теме вам — в энциклопедию. Если ещё не погружались, то маршрут прост:
- Основы
- Система и сеть
- Данные и разметка
- Код и разработка
- Языки
- Искусственный интеллект
- Проект
- Инфраструктура и безопасность
- Спин-офф
Обязательно пройдитесь.
А теперь приступим к нашему предмету.
Перед практикой откройте Работа с 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 секунд
- Скопируйте весь блок от
<!DOCTYPE html>до</html>. - Сохраните как
dom-demo.htmlи откройте двойным щелчком или перетащите в браузер. - Откройте Инструменты разработчика (
F12) → вкладка Console, если в примере естьconsole.log. - Меняйте селекторы и классы по одному — сразу видно, что сломалось.
| Где | Плюсы |
|---|---|
Локальный .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();
// сохранение..
});
Разбор. Несколько обработчиков на одном элементе не затирают друг друга. { once: true } — сработает один раз (удобно для «принять 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, { method: 'POST', body: new FormData(form) }).
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-анимации |