Знакомство с DOM-деревом проще всего начать с изучения структуры этого дерева.
Если коротко, DOM-дерево состоит из узлов (нод, node). Вместе узлы образуют иерархию, аналогичную HTML. При этом узлы делятся на два типа:
- Листовые — не содержат внутри себя других узлов
- Внутренние – у них есть узлы
Чаще всего конкретные узлы описывают конкретные теги из HTML и содержат их атрибуты внутри себя. У узлов есть тип, который определяет набор свойств и методов узла. В этом уроке мы с ними познакомимся.
Корневой элемент в DOM-дереве соответствует тегу <html>
. Доступ к нему можно получить так:
const html = document.documentElement;
// Свойство tagName узла содержит имя тега в верхнем регистре
console.log(html.tagName); // => 'HTML'
// Содержимое тега HTML в виде узлов DOM-дерева
// Текст тоже представлен узлом
html.childNodes; // [head, text, body]
// Потому что head выше body
html.firstChild; // <head>...</head>
html.lastChild; // <body>...</body>
// Второй узел, обращение по индексу
html.childNodes[1]; // #text
Теги <body>
и <head>
всегда присутствуют внутри документа, поэтому можно вынести их на уровень объекта document
для более простого доступа:
document.head;
document.body;
По дереву можно не только спускаться, но и подниматься:
// Родитель body это html
document.documentElement === document.body.parentNode; // true
document.body === document.body.childNodes[2].parentNode; // true
Если представить дерево, то по нему можно двигаться как вверх-вниз, так и влево-вправо. Картинка ниже это демонстрирует:
childNodes
Далее мы рассмотрим childNodes
– свойство, с помощью которого можно получить дочерние узлы — это узлы, вложенные в текущий узел на один уровень вложенности. Еще говорят, что это потомки первого уровня.
В работе с childNodes
есть несколько интересных моментов:
Это свойство доступно только для чтения. Попытка что-то записать в конкретный элемент не приведет к успеху:
// Ошибки не будет, но ничего не поменяется document.body.childNodes[0] = 'hey';
Изменить DOM-дерево можно с помощью специальных методов, которые мы изучим в соответствующем уроке.
Хотя
childNodes
и возвращает набор элементов, это все же не массив. В нем отсутствуют привычные методыmap()
,filter()
и другие. Но зато естьforEach()
:// Тип NodeList const nodes = document.documentElement.childNodes; nodes.forEach((el) => console.log(el));
Если очень хочется, то его можно преобразовать в массив, и затем уже работать привычным способом:
const list = Array.from(nodes); // Теперь у нас обычный массив и доступные методы, например, filter // Можем отфильтровать нужные элементы const filtered = list.filter((item) => item.textContent.includes('Навигация по DOM-дереву')); // И извлечь из них данные, например, имена тегов const filteredTags = filtered.map((item) => item.tagName); console.log(filteredTags); // => ['HEAD', 'BODY']
Иерархия
Узлы DOM-дерева не просто так делятся на типы. Эти типы выстраиваются в иерархию от общего к частному. В иерархии подтипы наследуют свойства и методы родительских типов и добавляют свои:
// Самый простой способ посмотреть тип
document.body.toString(); // "[object HTMLBodyElement]"
document.body instanceof HTMLBodyElement; // true
Узлы с типами Text и Comment являются листовыми, то есть они не могут иметь потомков. А вот элементы или производные типы от Element — это то, с чем приходится иметь дело чаще всего. К элементам относятся все типы, представленные тегами в HTML.
При работе с деревом естественным образом возникает понятие потомки. Применительно к DOM-дереву это означает, что тег с содержимым имеет потомков.
Посмотрите на пример кода:
const html = `
<html>
<head></head>
<body>
<div id="parent-div">
<h1>Заголовок</h1>
Привет!
<div class="child-div">
<span>Какой-то <b>текст</b></span>
<ol>
<li>1</li>
<li>2</li>
</ol>
<!-- End List -->
</div>
</div>
</body>
</html>
`;
В этом примере тег <div>
(с id parent-div
) содержит 14 потомков, в том числе три дочерних узла. Разберемся, в чем разница между этими понятиями.
Дочерними называют только те узлы, которые находятся на первом уровне вложенности. То есть дочерними будут считаться:
<h1>
- Текст
"Привет!"
<div>
с классомchild-div
Потомками называют все вложенные узлы на всех уровнях вложенности. Потомками тега <div>
(с id parent-div
) будут не только три вышеупомянутых дочерних тега, но и все узлы внутри них:
Дочерние узлы одновременно являются потомками. Обратное утверждение неверно — потомок необязательно является дочерним элементом. В нашем примере тег <span>
приходится потомком, но не дочерним элементом по отношению к тегу <div>
с id parent-div
.
Элементы
На практике чаще всего нас интересуют не любые узлы, а элементы. Именно ими мы манипулируем, перемещаемся сквозь них. Это настолько важно, что в DOM есть альтернативный способ обхода дерева, который построен только на элементах:
Все эти свойства возвращают объекты типа Element и пропускают объекты Text или Comment. Это видно в примере ниже, где свойство children
возвращает только теги.
Этим children
отличается от childNodes
, который возвращает все узлы, включая листовые:
const node = document.documentElement;
node.children; // [head, body]
Между children
и childNodes
есть еще одно довольно важное отличие. Они возвращают не только разный набор узлов, но и сам тип коллекции в первом и втором случае разный:
childNodes
возвращает NodeListchildren
— HTMLCollection
Они немного по-разному работают, но рассматривать эту разницу мы будем позже, когда познакомимся с селекторами.
Специальная навигация
Некоторые элементы обладают специальными свойствами для навигации по ним, к таким элементам относятся, например, формы и таблицы:
<table>
<tr>
<td>1.1</td>
<td>1.2</td>
<td>1.3</td>
</tr>
<tr>
<td>2.1</td>
<td>2.2</td>
<td>2.3</td>
</tr>
</table>
const table = document.body.firstElementChild;
table.rows[0].cells[2]; // <td>1.3</td>
В примере выше table
имеет специальные свойства для навигации rows
и cells
.
Этот способ навигации не заменяет основные способы. Он сделан исключительно для удобства в тех местах, где это имеет смысл. Например, таблица имеет ячейки в строках. Поэтому гораздо удобнее обращаться к строкам(англ. rows) и ячейкам(англ. cells) через индексы.
Заключение
Нужно ли все эти методы знать наизусть? В реальности — нет. Важно понимать общие принципы устройства DOM-дерева, знать иерархию типов и то, как принципиально происходит обход элементов. Конкретные методы и свойства всегда можно прочитать в документации. Наизусть их мало кто помнит, и в этом нет практического смысла.
Кроме того, обход дерева данными способами – это низкоуровневый способ работы. На практике для выборки нужных элементов используют селекторы, которые изучаются далее.
Самостоятельная работа
- Откройте консоль в своем браузере
- Начиная от
document.body
, доберитесь до самых глубоких узлов, содержащих этот текст
Дополнительные материалы
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
- Статья «Как учиться и справляться с негативными мыслями»
- Статья «Ловушки обучения»
- Статья «Сложные простые задачи по программированию»
- Вебинар «Как самостоятельно учиться»
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.