JavaScript DOM — 30 приёмов
Подборка готовых фрагментов для работы с DOM в браузере: скопировали HTML в файл, открыли в Chrome / Edge / Firefox — приём уже можно проверить. Каждый блок сопровождается разбором, зачем выбран именно этот API.
Основы DOM в браузере
Перед практикой откройте Работа с HTML в JavaScript — дерево узлов, шпаргалка API, связь с CSSOM. События, всплытие и делегирование — События в браузере. Каркас страницы — HTML-страницы целиком и HTML + CSS — макеты. Запросы к API — Fetch / axios. Дальше по стеку — React и Vue и Svelte (как в галерее Turtle — сначала основы, потом готовые рецепты).
Симулятор выше показывает, как 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 к моменту выполнения.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>DOM-пример</title>
<style>
*, *::before, *::after { box-sizing: border-box; }
body {
margin: 0;
padding: 1.5rem;
font-family: system-ui, sans-serif;
line-height: 1.5;
background: #f8fafc;
color: #1e293b;
}
</style>
</head>
<body>
<main id="app">
<!-- разметка примера -->
</main>
<script>
// ваш JavaScript
</script>
</body>
</html>
Разбор.
| Фрагмент | Смысл |
|---|---|
lang="ru" | Язык страницы для скринридеров и поисковиков |
id="app" | Удобная «корневая» точка для querySelector('#app') |
<script> в конце body | Элементы выше уже существуют — не нужен DOMContentLoaded для простых демо |
Стартовые приёмы
Три мини-примера, с которых обычно начинают урок по DOM.
Приветствие по кнопке
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Привет, DOM</title>
</head>
<body>
<button type="button" id="hi">Нажми</button>
<p id="out"></p>
<script>
const btn = document.getElementById('hi');
const out = document.getElementById('out');
btn.addEventListener('click', () => {
out.textContent = 'Привет из DOM!';
});
</script>
</body>
</html>
Разбор. 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 только отображает число. Так же работают счётчики лайков и корзины.
Переключение класса «активен»
<style>
.tab { padding: 0.5rem 1rem; border: 1px solid #cbd5e1; cursor: pointer; }
.tab.is-active { background: #2563eb; color: #fff; border-color: #2563eb; }
</style>
<button type="button" class="tab is-active" data-tab="a">Вкладка A</button>
<button type="button" class="tab" data-tab="b">Вкладка B</button>
<script>
document.querySelectorAll('.tab').forEach((tab) => {
tab.addEventListener('click', () => {
document.querySelectorAll('.tab').forEach((t) => t.classList.remove('is-active'));
tab.classList.add('is-active');
});
});
</script>
Разбор. Внешний вид меняется через 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. Раскрывающийся блок
<button type="button" id="faq-btn" aria-expanded="false" aria-controls="faq-panel">
Показать ответ
</button>
<div id="faq-panel" hidden>
<p>DOM — это дерево объектов HTML-страницы.</p>
</div>
<script>
const btn = document.getElementById('faq-btn');
const panel = document.getElementById('faq-panel');
btn.addEventListener('click', () => {
const open = btn.getAttribute('aria-expanded') === 'true';
btn.setAttribute('aria-expanded', String(!open));
panel.hidden = open;
});
</script>
Разбор. 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', () => {
rows().forEach((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 id="row-tpl">
<tr>
<td class="name"></td>
<td><button type="button" data-remove>Удалить</button></td>
</tr>
</template>
<table><tbody id="rows"></tbody></table>
<script>
function addRow(name) {
const tpl = document.getElementById('row-tpl');
const row = tpl.content.cloneNode(true);
row.querySelector('.name').textContent = name;
document.getElementById('rows').append(row);
}
addRow('Анна');
</script>
Разбор. Содержимое <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. Комментарии пользователя
<ul id="comments"></ul>
<input type="text" id="text">
<button type="button" id="send">Отправить</button>
<script>
const list = document.getElementById('comments');
document.getElementById('send').addEventListener('click', () => {
const text = document.getElementById('text').value.trim();
if (!text) return;
const li = document.createElement('li');
li.textContent = text; // НЕ innerHTML с сырым вводом
list.append(li);
document.getElementById('text').value = '';
});
</script>
Разбор.
| Способ | Риск при пользовательском вводе |
|---|---|
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-анимации |
См. также
Другие статьи этого же раздела в боковом меню (как на странице "О разделе"). Практическая карта типовых IT-задач: термины, пошаговое внедрение, проверка качества и типичные ошибки. Простой консольный чат на C# — учебное приложение с сокетами: TCP между клиентом и сервером, многопоточность и обмен сообщениями в консоли. Примеры вёрстки на HTML и CSS с разбором: центрирование, Flexbox, Grid, формы, шапка, подвал и адаптив для учебы и портфолио. Перед началом работы обязательно изучите главу Turtle . Галерея 3D-фигур на Panda3D — карточки, куб, пирамида, сфера, сетки и составные сцены; код для локального запуска. Готовые docker-compose.yml с разбором каждой строки — nginx, PostgreSQL, Redis, WordPress, MongoDB. Примеры для школьников и студентов: postgres example, поднять базу локально, app + db. Примеры nginx.conf для статики, reverse proxy, React/Vue SPA, PHP, SSL и балансировки — построчный разбор директив, проверка curl и типичные ошибки для лабораторных и VPS. dockerfile example — 10 готовых Dockerfile с построчным разбором: node, python, golang, react nginx, spring boot, php, dotnet. Для студентов, лабораторных и docker build с нуля. PromQL example — готовые запросы Prometheus и Grafana с построчным разбором: up, rate, node_exporter cpu, memory, disk, http_requests_total, histogram_quantile p99, алерты. Для студентов, лабораторных и devops docker compose. Готовые манифесты Kubernetes с разбором каждой строки — Pod, Deployment, Service, ConfigMap, Secret, Ingress. Примеры для Minikube, kind и kubectl apply. Примеры графиков Matplotlib на Python для школьников и студентов — sin, cos, парабола, столбцы, scatter, гистограмма, подграфики; код с подробным разбором. Примеры pandas на Python для школьников и студентов — DataFrame, фильтрация, groupby, очистка, merge, сводные таблицы и экспорт; код с подробным разбором каждой строки.Готовые решения
Простой консольный чат на CSharp
HTML + CSS — готовые макеты
Примеры фигур Turtle на Python
Примеры фигур Panda3D на Python
Docker Compose — готовые стеки
Nginx — конфиги под задачу
Dockerfile — 10 типовых образов
Prometheus + Grafana — запросы
Kubernetes YAML — минимальные манифесты
Matplotlib — графики
Pandas — типовые операции