Как обходить DOM в JavaScript: руководство для разработчиков

Зачем понимать обход DOM
Обход DOM — ключевой навык фронтенд-разработчика. Без умения выбирать узлы вы не сможете читать содержимое, изменять разметку или добавлять обработчики событий. Правильный выбор метода обхода уменьшит лишние операции, повысит производительность и упростит поддержку кода.
Коротко: DOM (Document Object Model) — древовидная модель HTML-документа. Каждый элемент, текст или комментарий — это узел (node). Терминология в одну строку: элемент — HTML-узел; узел может быть элементом, текстом или комментарием.
Основные направления обхода
- Вниз: от родителя к детям (childNodes, children, querySelector внутри элемента).
- Вверх: от потомка к родителю (parentElement, parentNode, closest).
- Вбок: между «соседями» (nextElementSibling, previousElementSibling).
Выбор направления зависит от того, какой узел у вас уже есть и какую цель вы преследуете. Частая практика — сначала найти опорный элемент (например, контейнер), затем искать внутри него — так вы ограничиваете область поиска.
Образец документа для практики
Ниже — пример HTML, на котором будем демонстрировать приёмы обхода:
Sample page
My Page Title
Nice caption goes here
List of amazing fruits
Must eat fruits
- Apples
- Oranges
- Avocados
-
Grapes
- Moon drops
- Sultana
- Concord
- Crimson Seedless
- Bananas
Amazing places in Kenya
Must visit places in Kenya
- Maasai Mara
- Diani Beach
- Watamu Beach
- Amboseli national park
- Lake Nakuru
Обход DOM вниз (к детям)
Способы:
- CSS-селекторы: element.querySelector / element.querySelectorAll
- children / childNodes
- firstElementChild / lastElementChild и firstChild / lastChild
Использование селекторов (querySelector / querySelectorAll)
querySelector позволяет найти первый узел, соответствующий селектору. querySelectorAll возвращает NodeList всех совпадений. Пример:
const headings = document.querySelectorAll('h2');
const firstHeading = headings[0]; // первый h2
const secondHeading = headings[1]; // второй h2
// получить содержимое
const orangeText = document.querySelector('.orange').innerHTML;Советы:
- Внутри уже найденного элемента можно вызвать element.querySelector — это ограничит область поиска.
- querySelectorAll обычно возвращает статический NodeList (не живой). Это значит, что после изменений DOM список не обновится автоматически.
children vs childNodes
- children — возвращает HTMLCollection только с элементами (теги), обычно «живой» (обновляется при изменении DOM).
- childNodes — возвращает все типы узлов (текст, комментарии и т.д.).
Пример:
const appleList = document.querySelector('.apple-list');
const apples = appleList.children;
console.log(apples); // HTMLCollection элементов liЕсли вам нужны только элементные узлы — используйте children или firstElementChild / lastElementChild.
firstChild / lastChild и аналоги
firstChild/lastChild возвращают первый/последний узел (может быть текстовым). Альтернативы firstElementChild/lastElementChild возвращают именно элементные узлы, что обычно удобнее в работе:
const appleList = document.querySelector('.apple-list');
const firstElem = appleList.firstElementChild; // элемент
const lastElem = appleList.lastElementChild; // элемент Обход DOM вверх (к родителям)
Способы:
- parentElement / parentNode
- closest(selector)
parentElement vs parentNode
- parentElement возвращает родителя, если он является элементом; если родитель — неэлементный узел, вернёт null.
- parentNode возвращает любой родительский узел (включая документ).
Пример:
const appleList = document.querySelector('.apple-list');
const parentDiv = appleList.parentElement;
console.log(parentDiv); // … closest — поднимаемся на несколько уровней
closest(selector) ищет ближайший родительский элемент (включая сам элемент), соответствующий селектору. Это удобно при обработке событий, когда вы хотите найти блок-контейнер:
const btn1 = document.querySelector('.btn-1');
const mainEl = btn1.closest('main');
console.log(mainEl); // элемент Замечание по совместимости: в старых браузерах для closest может потребоваться полифилл.
Обход DOM вбок (соседи)
- nextElementSibling / previousElementSibling — переход к следующему/предыдущему соседнему элементу.
- nextSibling / previousSibling — аналог, но учитывают любые узлы (включая текст).
Пример:
const orange = document.querySelector('.orange');
const apple = orange.previousElementSibling;
const avocado = orange.nextElementSibling;Комбинирование и цепочки
Часто нужно сочетать восхождение и спуск: сначала подняться к контейнеру, затем внутри него найти элемент. Цепочки делают код компактным:
const target = document.querySelector('.orange')
.closest('.first__article')
.querySelector('.btn-1');
// либо безопаснее с проверками
const orangeEl = document.querySelector('.orange');
const article = orangeEl && orangeEl.closest('.first__article');
const button = article && article.querySelector('.btn-1');Важно проверять наличие промежуточных узлов, чтобы избежать ошибок типа “Cannot read property of null”.
Практический приём: делегирование событий
Вместо назначения обработчика каждому элементу используйте делегирование: повесьте один обработчик на общий контейнер и находите целевой элемент через event.target.closest(selector).
Пример делегирования кликов по списку:
const wrapper = document.querySelector('.wrapper-1');
wrapper.addEventListener('click', (e) => {
const li = e.target.closest('li');
if (!li || !wrapper.contains(li)) return; // вышли, если не элемент списка
console.log('Clicked:', li.textContent.trim());
});Преимущества: экономия памяти, упрощение управления динамически добавляемыми элементами.
Сравнение методов (быстрое резюме)
| Метод | Что возвращает | Живая коллекция | Когда применять |
|---|---|---|---|
| querySelector / querySelectorAll | Элементы по CSS-селектору | querySelectorAll — статичный NodeList | Универсальный поиск, внутри элемента для ограничения области |
| getElementById / getElementsByClassName | Элемент / HTMLCollection | getElementsByClassName — живая | Быстро найти по id или классу, особенно когда ожидаются изменения DOM |
| children | Только дочерние элементы | Да, HTMLCollection (живая) | Когда нужны только теги-потомки |
| childNodes | Все дочерние узлы (включая текст) | Да | Когда важны текстовые узлы или комментарии |
| parentElement / parentNode | Родительский узел | N/A | Подъём на 1 уровень вверх |
| closest | Ближайший родитель по селектору | N/A | Быстрый поиск контейнера выше по дереву |
| nextElementSibling / previousElementSibling | Ближайшие соседние элементы | N/A | Навигация между элементами одинакового уровня |
Частые ошибки и когда методы подводят
- Ожидание, что querySelectorAll вернёт «живой» список: нет — он статичный.
- Использование firstChild вместо firstElementChild приводит к получению текстового узла (например, перевода строки).
- Не учитывают, что parentElement может вернуть null (если родитель — не элемент).
- Плохая проверка существования при цепочках ведёт к исключениям.
Производительность и рекомендации
- Ограничивайте область поиска: вместо document.querySelector используйте contextElement.querySelector.
- Для списка часто изменяемых элементов используйте делегирование событий.
- Если нужна актуальная коллекция, используйте getElementsByClassName или children; если нужна «моментальная» снимок — querySelectorAll.
- Избегайте частого перебора всего документа (document.querySelectorAll(‘*’)).
Тесты и критерии приёмки
Критерии приёмки для функционала, связанного с обходом DOM:
- Функция должна корректно возвращать ожидаемый элемент в пределах заданного контейнера.
- Логика должна учитывать отсутствие узла и не выбрасывать исключения.
- При динамическом добавлении элементов обработчики через делегирование должны работать без переназначения.
- Негативные кейсы: поведение при пустом селекторе, отсутствующем контейнере и неэлементных родителях проверены.
Минимальные тесты:
- Убедиться, что querySelectorAll возвращает ожидаемое количество элементов.
- Проверить, что childNodes содержит текстовые узлы, а children — только элементы.
- Проверить работу closest при нескольких уровнях вложенности.
Ролевые чек-листы
Для разработчика:
- Проверить область поиска (ограничить контейнером).
- Использовать classList вместо манипуляций с className, где возможно.
- Добавить проверки на null при цепочках.
- Написать unit/интеграционные тесты для ключевых сценариев.
Для ревьюера кода:
- Убедиться в отсутствии повторной выборки элементов внутри циклов.
- Проверить наличие делегирования событий для динамических списков.
- Оценить производительность и возможные утечки памяти (проверить removeEventListener).
Чек-лист безопасности и производительности
- Не храните ссылки на удалённые DOM-узлы без необходимости.
- Удаляйте обработчики при размонтировании компонентов.
- Не используйте innerHTML для вставки данных из ненадёжных источников — это XSS-угроза.
Быстрый шпаргалка (cheat sheet)
- document.querySelector(‘.class’) — первый элемент
- document.querySelectorAll(‘li’) — NodeList (статичный)
- element.children — HTMLCollection (только элементы, живой)
- element.childNodes — NodeList (включая текст)
- element.firstElementChild / lastElementChild — первый/последний элемент
- node.parentElement / node.parentNode — родитель
- element.closest(‘.block’) — ближайший родитель с классом
- node.nextElementSibling / previousElementSibling — соседний элемент
Примеры реального применения
- Поиск контейнера и отображение модалки внутри этого контейнера.
- Делегирование кликов в списке с динамически подгружаемыми элементами.
- Поиск ближайшего .form-group от кнопки Submit для валидации.
Совместимость и полифиллы
Большинство методов поддерживаются современными браузерами. Для старых версий (например, устаревшие Internet Explorer) может потребоваться полифилл для Element.closest и других новых API. Всегда тестируйте критичные сценарии в целевых браузерах.
Короткий глоссарий
- Элемент (Element) — HTML-узел (теги).
- Узел (Node) — любой узел DOM (элемент, текст, комментарий).
- HTMLCollection — живой массивоподобный объект элементов.
- NodeList — коллекция узлов; бывает статичной или живой в разных случаях.
Заключение — что важно помнить
- Ограничивайте область поиска и используйте правильные методы для вашей задачи.
- Помните про живые коллекции и статичные снимки (querySelectorAll vs getElementsByClassName/children).
- Делегирование событий и использование closest помогают писать более устойчивый и лёгкий код.
Короткие рекомендации:
- Найдите ближайший статичный контейнер; 2) выполняйте выборку внутри него; 3) используйте делегирование там, где список динамический; 4) добавляйте защиту от null при цепочках.
Сводка
- DOM traversal — базовый навык для управления структурой страницы.
- Правильный выбор метода влияет на читаемость, производительность и устойчивость к изменениям DOM.
- Проверяйте совместимость и избегайте XSS при работе с innerHTML.
Похожие материалы
Переход с Microsoft Office на WPS Office
Как изменить цвет текста с помощью CSS
CSS тени: box-shadow и text-shadow
Как встроить MP3 на сайт — HTML5, Google Drive, CMS
Начать сайт с HTML5 Boilerplate — быстрое руководство