Полное руководство по обходу DOM в JavaScript

Что такое обход DOM
DOM (Document Object Model) — это представление HTML-документа в виде дерева узлов. Каждый HTML-элемент, текстовая часть и комментарий — это узел. DOM предоставляет API для доступа и изменения структуры, содержимого и стилей страницы с помощью JavaScript.
Определение термина: узел — элемент дерева DOM (элемент, текст, комментарий и т.д.).
Обход DOM (DOM traversal) — выборка узлов относительно другого узла: движения вниз к детям, вверх к родителям и в стороны к соседям. Часто эффективнее начать поиск от уже известного узла, чем сканировать весь документ.
Важно: некоторые коллекции в DOM «живые» (live), а некоторые — статические. Разница влияет на поведение при изменении дерева.
Кому это нужно
- Frontend-разработчикам: для выборки элементов и управления интерфейсом.
- Тестировщикам: для написания селекторов в end-to-end тестах.
- Инженерам по производительности: для оптимизации операций манипуляции DOM.
Пример документа для практики
Ниже — пример 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
Движение вниз по дереву
Движение вниз означает переход от родителя к дочерним узлам. Наиболее распространённые способы:
- selector methods: element.querySelector / element.querySelectorAll
- свойства children и childNodes
- специальные свойства firstChild и lastChild
Селекторные методы
querySelector и querySelectorAll принимают CSS-селектор и ищут элементы внутри текущего узла.
Примеры:
const firstArticle = document.querySelector('.first__article');
const allH2 = document.querySelectorAll('h2');
const firstH2 = allH2[0];
const secondH2 = allH2[1];Особенности:
- querySelector возвращает первый совпавший элемент или null.
- querySelectorAll возвращает статический NodeList (не «живой»).
- Внутри селектора используйте CSS-символы: “.class” для классов, “#id” для id, и т.д.
Чтобы получить содержимое элемента, используйте свойства innerHTML, textContent или value (для input):
document.querySelector('.orange').innerHTMLСовет по доступности: для извлечения текста предпочитайте textContent — он возвращает текст без разметки.
children vs childNodes
- children возвращает HTMLCollection только с дочерними элементами (элементными узлами).
- childNodes возвращает NodeList со всеми дочерними узлами, включая текстовые (например пробелы и переносы строк), комментарии.
Пример:
const appleList = document.querySelector('.apple-list');
const apples = appleList.children; // HTMLCollection
console.log(apples[0]);Замечание: HTMLCollection — «живой», то есть при изменении DOM коллекция обновляется автоматически. NodeList от querySelectorAll — обычно статический.
firstChild и lastChild
Эти свойства возвращают первый и последний дочерний узел (node). Если вам нужны именно элементные узлы, лучше использовать firstElementChild / lastElementChild.
const appleList = document.querySelector('.apple-list');
const first = appleList.firstElementChild; // безопаснее, возвращает элемент
const last = appleList.lastElementChild;Используйте firstChild/lastChild только если вы ожидаете текстовые узлы или комментарии.
Движение вверх по дереву
Навигация к родительским узлам нужна, когда вы находитесь внутри элемента и хотите найти ближайший контейнер.
Основные методы:
- parentElement / parentNode
- closest
parentElement vs parentNode
- parentElement возвращает родительский элемент или null, если родитель — не элемент.
- parentNode возвращает родительский узел любого типа (например, DocumentFragment).
Пример:
const appleList = document.querySelector('.apple-list');
const parentDiv = appleList.parentElement; // div.wrapper-1closest
closest поднимается вверх по дереву и возвращает ближайший предок, соответствующий селектору. Если совпадений нет — null.
const btn1 = document.querySelector('.btn-1');
const mainEl = btn1.closest('main');closest полезен, когда компонент может быть вложен глубоко, а вам нужен ближайший контейнер по семантике.
Движение в стороны
Чтобы перемещаться между элементами на одном уровне (соседями), используйте:
- nextElementSibling / previousElementSibling — возвращают следующий/предыдущий элементный узел.
- nextSibling / previousSibling — возвращают любой следующий/предыдущий узел (включая текст).
Пример:
const orange = document.querySelector('.orange');
const apple = orange.previousElementSibling;
const avocado = orange.nextElementSibling;Используйте Element-sibling версии для предсказуемого поведения с элементными узлами.
Чейннинг свойств и методов
Комбинируя методы, можно переходить сразу в нужную ветку дерева:
const grapesType1 = document
.querySelector('.first__article')
.querySelector('.apple-list')
.querySelector('.grape')
.querySelector('.type-1');Предупреждение: длинные цепочки без проверок могут выбрасывать ошибки, если промежуточный узел равен null. Для безопасного обхода используйте проверки или optional chaining:
const maybeType1 = document
.querySelector('.first__article')
?.querySelector('.apple-list')
?.querySelector('.grape')
?.querySelector('.type-1');
if (maybeType1) {
// безопасно работать
}Частые ошибки и когда методы не работают
- Пробелы и переносы в HTML добавляют текстовые узлы, и childNodes может вернуть неожиданные элементы.
- Использование firstChild/lastChild без ожидания текстовых узлов приводит к отсутствию нужного элемента.
- querySelectorAll возвращает статический NodeList — изменения DOM после вызова не отобразятся в нём.
- parentElement вернёт null для корневого узла или если родитель — не элемент.
- Фрагменты DocumentFragment и Shadow DOM имеют свои особенности: некоторые глобальные селекторы не работают внутри shadow root.
Пример ошибки:
const list = document.querySelector('.apple-list');
const first = list.firstChild; // может быть текстовый узел (пробел), не
console.log(first.nodeType); // 3 — текстовый узел Решение: используйте firstElementChild или фильтруйте по nodeType === 1.
Производительность и выбор метода
- document.getElementById / getElementsByClassName / getElementsByTagName обычно быстрее для простых селекторов, но современные движки оптимизируют querySelectorAll.
- Если вы повторно используете результат, кэшируйте ссылку на элемент, вместо частых вызовов селекторов.
- Для больших документов ограничьте область поиска: вызывайте querySelector на контейнере, а не на document.
Правило: минимизируйте количество обращений к DOM и скопируйте элементы во фрагмент (DocumentFragment) при массовой вставке.
Шаблоны и рекомендации
Мини-методология для безопасных манипуляций с DOM:
- Найдите ближний стабильный контейнер (id или уникальный класс).
- Ограничьте поиск этим контейнером.
- Используйте element.querySelector/querySelectorAll для сложных селекторов.
- Проверяйте результат перед доступом к свойствам (null-check).
- Для частых обновлений извлекайте данные, изменяйте в памяти, затем применяйте единым обновлением.
Шаблон доступа с проверкой:
function getTextOf(selector, root = document) {
const el = root.querySelector(selector);
return el ? el.textContent.trim() : null;
}Шаблоны для событий
- Делегирование событий: вешайте обработчик на контейнер и определяйте целевой элемент через event.target.closest.
document.querySelector('.apple-list').addEventListener('click', (e) => {
const li = e.target.closest('li');
if (!li) return;
// обработка клика по li
});Преимущество: меньше обработчиков, лучше производительность и простота динамически созданных элементов.
Рольовые контрольные списки
Разработчик:
- Кэшировать элементы при повторном использовании.
- Использовать querySelector внутри минимального контейнера.
- Проверять существование узлов перед доступом.
- Предпочитать textContent для извлечения текста.
Тестировщик QA:
- Проверить селекторы на устойчивость при изменении разметки.
- Убедиться, что делегирование событий работает для динамически добавляемых элементов.
- Проверить поведение в старых браузерах, если проект поддерживает их.
Архитектор:
- Определить контракт DOM: какие классы/атрибуты используются как селекторы.
- Рекомендовать уникальные идентификаторы для критичных компонентов.
- Минимизировать использование селекторов, зависящих от структуры, если структура меняется часто.
Критерии приёмки
- Функция выборки возвращает корректный элемент при ожидаемой структуре.
- Ненайденные элементы обрабатываются без ошибок (возврат null или безопасное поведение).
- Для массовых операций изменения DOM минимизированы (атомарная вставка через DocumentFragment).
- Селекторы устойчивы к незначительным изменениям разметки (тесты покрывают ключевые сценарии).
Примеры анти-паттернов и альтернативы
Анти-паттерн: полагаться на порядок детей (например, всегда брать children[2]) — хрупко при изменении верстки.
Альтернатива: использовать семантические селекторы или data-атрибуты:
Apples И затем:
const primary = document.querySelector('[data-role="primary-fruit"]');Это делает селектор устойчивым к перестановкам и перестраиваниям DOM.
Совместимость и миграция
- optional chaining (?.) поддерживается в современных браузерах. Для старых окружений используйте полифиллы или явные проверки.
- Для работы внутри Shadow DOM методы querySelector работают в пределах shadow root, но глобальные document-селектора туда не доберутся.
- Если поддерживаете IE11, избегайте optional chaining и некоторых новых API; используйте feature detection.
Практические приёмы и советы
- Используйте firstElementChild / lastElementChild для предсказуемости.
- Для поиска по тексту применяйте XPath только в редких случаях — XPath выражения сложнее и реже используются.
- При динамической вставке создавайте шаблоны и клонируйте содержимое.
Пример использования шаблона:
const tpl = document.querySelector('#item-template');
const clone = tpl.content.cloneNode(true);
clone.querySelector('.title').textContent = 'Новая запись';
document.querySelector('.list').appendChild(clone);Быстрый справочник (cheat sheet)
- Найти первый элемент: document.querySelector(selector)
- Найти все элементы: document.querySelectorAll(selector)
- Дочерние элементы: el.children
- Все дочерние узлы: el.childNodes
- Первый дочерний элемент: el.firstElementChild
- Предыдущий сосед: el.previousElementSibling
- Родительский элемент: el.parentElement
- Ближайший предок по селектору: el.closest(selector)
Когда обход не подходит — альтернативы
- Резкое изменение UI: использовать виртуальный DOM (React/Vue) — фреймворки управляют обходом и рендерингом.
- Манипуляции с большими объёмами: формировать HTML-строку или использовать серверный рендеринг, чтобы уменьшить количество операций в браузере.
Короткое руководство по отладке
- Откройте DevTools → Elements и найдите интересующий узел вручную.
- В Console выполните document.querySelector(‘…’) и посмотрите возвращаемое значение.
- Используйте console.dir() для изучения свойств элемента.
- Вставляйте временные атрибуты data-test в разметку, чтобы упростить селекторы для тестов.
Заключение
Обход DOM — базовый навык для веб-разработчика. Понимание направлений (вниз, вверх, в стороны), разницы между типами коллекций и методов помогает писать более устойчивый, производительный и читабельный код. Используйте делегирование событий, кэширование и семантические селекторы, чтобы избежать хрупких зависимостей от структуры разметки.
Важно
- Проверяйте значения на null и предпочитайте element-версии свойств (firstElementChild, nextElementSibling) для предсказуемости.
Краткий план действий
- Начните с поиска ближайшего стабильного контейнера.
- Ограничьте область поиска.
- Используйте безопасный чейн с проверками или optional chaining.
- При массовых изменениях работайте с DocumentFragment.
Дополнительно
- Ресурсы для изучения: MDN Web Docs (разделы по DOM), статьи по оптимизации манипуляций DOM.