Навигация по дереву: родители, дети, соседи
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
В DOM нельзя шагнуть «куда-то туда»: от любого элемента есть ровно три направления — вверх к родителю, вниз к детям и вбок к соседям. Разберём эти шаги и заодно поймём, почему у половины свойств в названии торчит слово Element.
Когда браузер разбирает разметку, в дерево попадают не только теги. Текст между ними и комментарии — тоже узлы, полноправные объекты со своими свойствами. Элемент (тег) — всего лишь один сорт узла: самый заметный, но не единственный.
const box = document.getElementById("box"); console.log(box.childNodes.length); console.log(box.children.length); console.log(box.childNodes[0].nodeName); console.log(box.childNodes[2].nodeName);Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Отсюда двойной набор свойств навигации. Одни ходят по всем узлам подряд, другие пропускают текст и комментарии и видят только теги. В прикладном коде почти всегда нужен второй набор — тот, где в названии есть Element.
| Шаг | Любые узлы | Только элементы |
|---|---|---|
| вниз, все дети | childNodes | children |
| вниз, первый | firstChild | firstElementChild |
| вниз, последний | lastChild | lastElementChild |
| вверх | parentNode | parentElement |
| вбок вправо | nextSibling | nextElementSibling |
| вбок влево | previousSibling | previousElementSibling |
Откуда они берутся, подробно показано в статье про дерево документа: каждый промежуток между тегами — перенос строки с отступом — становится отдельным текстовым узлом и честно попадает в childNodes. Здесь важно только следствие: «пять детей» у списка из двух пунктов — норма, а не поломка.
const ul = document.createElement("ul"); ul.innerHTML = "\n <li>A</li>\n <li>B</li>\n"; console.log(ul.childNodes.length); console.log(ul.children.length); console.log(ul.firstChild.nodeName); console.log(ul.firstElementChild.nodeName);Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
children — коллекция дочерних элементов в порядке разметки, с доступом по индексу и length. У краёв есть отдельные имена: firstElementChild и lastElementChild, а посчитать детей можно через childElementCount.
const menu = document.getElementById("menu"); const items = menu.children; console.log(items.length); console.log(menu.firstElementChild.textContent); console.log(menu.lastElementChild.textContent); menu.append(document.createElement("li")); console.log(items.length);Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Последняя строка — не опечатка. Коллекция живая: переменная items хранит не снимок, сделанный в момент обращения, а окно в дерево. Появился новый ребёнок — length изменился сам, хотя к items мы больше не обращались.
children — не массив. Ни map, ни forEach у него нет: перебирайте через for...of, а если нужны методы массива, сделайте копию: Array.from(el.children) или [...el.children].
Наверх ведёт parentElement — тот элемент, внутри которого мы находимся. Путь наверх конечен и короток: обычно несколько шагов до <body> и упор в <html>.
const text = document.getElementById("text"); console.log(text.parentElement.id); console.log(text.parentElement.tagName); console.log(document.documentElement.parentNode.nodeName); console.log(document.documentElement.parentElement);Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
parentNode и parentElement расходятся там, где родитель — не элемент. Первый такой случай — вершина: родитель <html> — сам объект документа, а документ не элемент, поэтому parentElement честно отдаёт null. Второй — узел, который в документ ещё не вставлен: его родителем может оказаться DocumentFragment, тоже не элемент. Внутри дерева документа оба свойства дают одно и то же.
Шагать наверх вручную приходится редко: closest(селектор) проходит путь сам и возвращает первого подходящего предка. Проверку он начинает с самого элемента, а если подходящего предка нет — возвращает null.
const tag = document.getElementById("tag"); console.log(tag.closest(".card").id); console.log(tag.closest("p").tagName); console.log(tag.closest("span") === tag); console.log(tag.closest("table"));Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Соседи — элементы с тем же родителем. nextElementSibling — следующий справа, previousElementSibling — предыдущий слева. Перепрыгнуть через двоих нельзя: шаг всегда ровно на одну позицию.
const first = document.getElementById("first"); const last = document.getElementById("last"); console.log(first.nextElementSibling.textContent); console.log(first.previousElementSibling); console.log(last.previousElementSibling.textContent); console.log(last.nextElementSibling);Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
У краёв ряда соседа нет, и вместо элемента приходит null. Это не ошибка и не пустая строка, а признак конца — на нём удобно останавливать цикл.
Обход ряда складывается из двух готовых деталей: firstElementChild даёт старт, nextElementSibling — шаг, а null сам сообщает, когда пора остановиться.
let node = document.getElementById("steps").firstElementChild; const titles = []; while (node !== null) { titles.push(node.textContent); node = node.nextElementSibling; } console.log(titles.join(" | ")); console.log(node);Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Вглубь так не пройти: у каждого ребёнка могут быть свои дети, а у тех — свои. Дерево обходят рекурсией — функция вызывает саму себя для каждого потомка и останавливается там, где детей больше нет.
function countElements(el) { let total = 0; for (const child of el.children) { total += 1 + countElements(child); } return total; } const root = document.getElementById("root"); console.log(root.children.length); console.log(countElements(root));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 и так далее. Считать нужно только элементы, текстовые узлы не в счёт.
function indexOf(el) { // ваш код: посчитайте, сколько элементов стоит слева return 0; } console.log(indexOf(document.getElementById("target"))); // сейчас 0, должно быть 1Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Соседей слева достаточно пересчитать: шагаем по previousElementSibling, пока не придёт null.
function indexOf(el) { let i = 0; let prev = el.previousElementSibling; while (prev !== null) { i += 1; prev = prev.previousElementSibling; } return i; } console.log(indexOf(document.getElementById("target"))); // 1Листинг только для чтения: этот код не запускается в браузере.
Тот же ответ даёт [...el.parentElement.children].indexOf(el), но цикл обходится без копии коллекции в массив.
Свой closest
Соберите closest руками: функция upTo(el, selector) поднимается по parentElement и возвращает первый элемент, подходящий под селектор, начиная с самого el. Если такого нет — null. Проверить совпадение помогает el.matches(selector).
function upTo(el, selector) { // ваш код: поднимайтесь наверх, пока элемент не совпадёт с селектором return null; } const found = upTo(document.getElementById("deep"), ".widget"); console.log(found === null ? "не найдено" : found.id); // сейчас: не найдено, должно быть wTab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
Проверяем сам элемент, потом поднимаемся на шаг выше — и так, пока родитель-элемент есть.
function upTo(el, selector) { let current = el; while (current !== null) { if (current.matches(selector)) { return current; } current = current.parentElement; } return null; } console.log(upTo(document.getElementById("deep"), ".widget").id); // wЛистинг только для чтения: этот код не запускается в браузере.
Цикл останавливается сам: у <html> нет родителя-элемента, current становится null, и функция возвращает null — ровно как настоящий closest.