Навигация по дереву: родители, дети, соседи

окружение≈ 6 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

В DOM нельзя шагнуть «куда-то туда»: от любого элемента есть ровно три направления — вверх к родителю, вниз к детям и вбок к соседям. Разберём эти шаги и заодно поймём, почему у половины свойств в названии торчит слово Element.

Разделы темы:·

Когда браузер разбирает разметку, в дерево попадают не только теги. Текст между ними и комментарии — тоже узлы, полноправные объекты со своими свойствами. Элемент (тег) — всего лишь один сорт узла: самый заметный, но не единственный.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Отсюда двойной набор свойств навигации. Одни ходят по всем узлам подряд, другие пропускают текст и комментарии и видят только теги. В прикладном коде почти всегда нужен второй набор — тот, где в названии есть Element.

ШагЛюбые узлыТолько элементы
вниз, все детиchildNodeschildren
вниз, первыйfirstChildfirstElementChild
вниз, последнийlastChildlastElementChild
вверхparentNodeparentElement
вбок вправоnextSiblingnextElementSibling
вбок влевоpreviousSiblingpreviousElementSibling

Откуда они берутся, подробно показано в статье про дерево документа: каждый промежуток между тегами — перенос строки с отступом — становится отдельным текстовым узлом и честно попадает в childNodes. Здесь важно только следствие: «пять детей» у списка из двух пунктов — норма, а не поломка.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

children — коллекция дочерних элементов в порядке разметки, с доступом по индексу и length. У краёв есть отдельные имена: firstElementChild и lastElementChild, а посчитать детей можно через childElementCount.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Последняя строка — не опечатка. Коллекция живая: переменная items хранит не снимок, сделанный в момент обращения, а окно в дерево. Появился новый ребёнок — length изменился сам, хотя к items мы больше не обращались.

children — не массив. Ни map, ни forEach у него нет: перебирайте через for...of, а если нужны методы массива, сделайте копию: Array.from(el.children) или [...el.children].

Наверх ведёт parentElement — тот элемент, внутри которого мы находимся. Путь наверх конечен и короток: обычно несколько шагов до <body> и упор в <html>.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

parentNode и parentElement расходятся там, где родитель — не элемент. Первый такой случай — вершина: родитель <html> — сам объект документа, а документ не элемент, поэтому parentElement честно отдаёт null. Второй — узел, который в документ ещё не вставлен: его родителем может оказаться DocumentFragment, тоже не элемент. Внутри дерева документа оба свойства дают одно и то же.

Шагать наверх вручную приходится редко: closest(селектор) проходит путь сам и возвращает первого подходящего предка. Проверку он начинает с самого элемента, а если подходящего предка нет — возвращает null.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Соседи — элементы с тем же родителем. nextElementSibling — следующий справа, previousElementSibling — предыдущий слева. Перепрыгнуть через двоих нельзя: шаг всегда ровно на одну позицию.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

У краёв ряда соседа нет, и вместо элемента приходит null. Это не ошибка и не пустая строка, а признак конца — на нём удобно останавливать цикл.

Обход ряда складывается из двух готовых деталей: firstElementChild даёт старт, nextElementSibling — шаг, а null сам сообщает, когда пора остановиться.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Вглубь так не пройти: у каждого ребёнка могут быть свои дети, а у тех — свои. Дерево обходят рекурсией — функция вызывает саму себя для каждого потомка и останавливается там, где детей больше нет.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

  • Узлы — всё содержимое дерева: теги, текст, комментарии. Элементы — только теги, частный случай узла.
  • Правило по умолчанию: берите вариант со словом Element, иначе в выборку попадут переносы строк и отступы.
  • Вниз — children, firstElementChild, lastElementChild; вверх — parentElement (и closest с селектором); вбок — nextElementSibling и previousElementSibling.
  • children — живая коллекция, а не массив: перебор через for...of, методы массива — после Array.from.
  • Край ряда и вершина дерева дают null — на этом признаке и держится цикл обхода, а вглубь дерево обходят рекурсией.

1/3В разметке <div id="a">Текст <b>жирный</b></div> — что вернут a.childNodes.length и a.children.length?

2/3Почему у элемента <html> свойство parentNode есть, а parentElement возвращает null?

3/3Как надёжнее всего остановить цикл, шагающий по nextElementSibling?

Номер элемента среди соседей

Напишите функцию indexOf(el), которая возвращает порядковый номер элемента среди братьев-элементов: у первого — 0, у второго — 1 и так далее. Считать нужно только элементы, текстовые узлы не в счёт.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Соседей слева достаточно пересчитать: шагаем по previousElementSibling, пока не придёт null.

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Тот же ответ даёт [...el.parentElement.children].indexOf(el), но цикл обходится без копии коллекции в массив.

Свой closest

Соберите closest руками: функция upTo(el, selector) поднимается по parentElement и возвращает первый элемент, подходящий под селектор, начиная с самого el. Если такого нет — null. Проверить совпадение помогает el.matches(selector).

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Проверяем сам элемент, потом поднимаемся на шаг выше — и так, пока родитель-элемент есть.

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Цикл останавливается сам: у <html> нет родителя-элемента, current становится null, и функция возвращает null — ровно как настоящий closest.