Перейти к основному содержимому

JavaScript DOM — 30 приёмов

Подборка готовых фрагментов для работы с DOM в браузере: скопировали HTML в файл, открыли в Chrome / Edge / Firefox — приём уже можно проверить. Каждый блок сопровождается разбором, зачем выбран именно этот API.


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

Сначала теория

Перед практикой откройте Работа с HTML в JavaScript — дерево узлов, шпаргалка API, связь с CSSOM. События, всплытие и делегирование — События в браузере. Каркас страницы — HTML-страницы целиком и HTML + CSS — макеты. Запросы к API — Fetch / axios. Дальше по стеку — React и Vue и Svelte (как в галерее Turtle — сначала основы, потом готовые рецепты).

Загрузка демо DOM…

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

Загрузка HTML-песочницы…

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


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

ТерминПростыми словами
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 приёмов

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

Разбор. Несколько обработчиков на одном элементе не затирают друг друга. &#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', () => {
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 из формы и fetchFetch / axios
React-компонентыReact — рецепты
Vue, Svelte, реактивный UIVue и Svelte — компоненты
Анимация без тяжёлого JSCSS-анимации

См. также

Другие статьи этого же раздела в боковом меню (как на странице "О разделе").

Освоение главы0%