Навигация по дереву DOM

На этом занятии рассмотрим способы навигации по DOM дереву и выбора нужного элемента в HTML-документе. Предположим, что у нас имеется вот такая страница:

<!DOCTYPE html>
<html>
<head>
    <title>Уроки по JavaScript</title>
</head>
<body>
Абзац
<ul>
<li>Элемент списка</li>
</ul>
</body>
</html>

Мы не будем рисовать DOM-дерево этого документа, вы теперь можете делать это самостоятельно и, к тому же, сейчас в этом нет необходимости. Как мы говорили на предыдущем занятии, точкой входа для DOM является объект document. Чтобы с его помощью выбрать теги-объекты <html>, <head> и <body>, используются следующие свойства:

let html = document.documentElement;
let body = document.body;
let head = document.head;
 
console.log(html, body, head);

Для наглядности я буду писать программы непосредственно в тегах <script></script>, которые пропишем в конце документа:

<script></script>

Обратите внимание, что мы прописали тег <script> в конце документа. Это сделано для того, чтобы браузер успел загрузить весь документ и создать полное DOM-дерево. Если мы его подключим, например, в разделе head, то переменная body будет равна null (что означает, что объект не существует). Вот это следует учитывать при работе с DOM: нужно быть уверенным, что браузер полностью загрузил документ, прежде чем работать с ним через JavaScript.

Дочерние узлы и потомки

Теперь посмотрим, как можно перебирать вложенные элементы этого дерева. Для начала введем два термина, чтобы мы понимали друг друга:

  • дочерние узлы (или дети) – элементы, которые являются непосредственными детьми узла;
  • потомки – все элементы, которые лежат внутри данного, включая детей, детей их детей и т.д.

Например, вот в этом фрагменте дерева head является дочерним узлом html, но title уже им не является, так как вложен в head. А вот потомки – это все вложенные элементы, значит, для html потомками будут и head и title. Вот это и гласят данные определения.

Для каждого узла дерева список его дочерних элементов можно получить через коллекцию childNodes, например, так:

for (let i = 0; i < document.body.childNodes.length; i++) {
    console.log( document.body.childNodes[i] );
}

Коллекция childNodes представляет собой псевдомассив, то есть, это итерируемый объект, но в нем отсутствуют методы массива. Из предыдущих занятий мы знаем, что для перебора элементов псевдомассива лучше использовать цикл for of:

for (let child of document.body.childNodes) {
    console.log( child );
}

Некоторые начинающим программисты применяют цикл for in для этой коллекции, что не стоит делать, по крайней мере, по двум причинам: это будет работать медленнее; будут доступны дополнительные свойства childNodes, которые обычно не нужны.

Если же мы хотим оперировать коллекцией childNodes как массивом, то ее можно преобразовать в массив:

let arr = Array.from(document.body.childNodes);

и, затем, использовать любые методы массивов, например:

arr.forEach((elem) => console.log(elem));

У каждого узла DOM-дерева имеются свойства firstChild и lastChild. Они обеспечивают доступ к первому и последнему дочернему элементу в списке childNodes. Например, так:

let html = document.documentElement;
let first = html.firstChild;
let last = html.lastChild;
 
console.log( first );         //head
console.log( last );          //body

По сути, это то же самое, что и такой выбор:

let html = document.documentElement;
let first = html.childNodes[0];
let last = html.childNodes[html.childNodes.length-1];
 
console.log( first );
console.log( last );

но записано в более краткой форме.

Если нам нужно узнать, есть ли у текущего элемента дочерние узлы, то используется метод hasChildNodes():

let html = document.documentElement;
console.log( html.hasChildNodes() );

Возвращает true, если есть и false, если нет.

Обратите внимание, что элементы коллекции childNodes доступны только для чтения. Менять их, используя синтаксис:

document.body.childNodes[0] = document.head.childNodes[0];

нельзя. Для удаления, добавления и изменения узлов дерева используются свои методы, о которых речь пойдет позже.

Наконец, если нам нужен только список дочерних тегов, то вместо коллекции childNodes используется коллекция children:

for (let child of document.documentElement.children) {
   console.log( child );
}

Теперь, мы видим только список тегов.

Соседние элементы, родитель

Теперь поговорим о переходах между соседними элементами. И введем такое понятие:

Соседи – это узлы, у которых один и тот же родитель.

Например, вот в такой базовой структуре документа:

<html>
  <head>...</head><body>...</body>
</html>

объекты head и body являются соседями, т.к. у них общий родитель – объект html. Причем, для head объект body является следующим правым, а для body объект head – предыдущий слева. Отсюда название двух свойств:

  • nextSibling – следующий сосед (справа);
  • previousSibling – предыдущий сосед (слева).

Например (структура HTML-документа должна быть как выше):

console.log(document.head.nextSibling);
console.log(document.body.previousSibling);

В консоле увидим объект body и head.

Наконец, если нам нужно получить объект родителя, то используется свойство parentNode:

console.log(document.body.parentNode);

Итак, относительно любого узла DOM-дерева можно осуществлять следующие переходы:

Если же нам нужны только узлы-элементы (как правило, это теги HTML-документа), то используется похожий набор свойств:

Здесь есть только одно ключевое отличие этих свойств (а, точнее, одного свойства parentElement), от ранее рассмотренных. В целом и parentNode и parentElement возвращают всегда один и тот же родительский элемент, кроме корневого элемента:

let html = document.documentElement;
console.log( html.parentNode ); // выведет document
console.log( html.parentElement ); // выведет null

Объект document не считается элементом HTML-страницы, поэтому ссылка parentElement для корня DOM-дерева возвращает null.

Некоторые типы DOM-элементов предоставляют для удобства дополнительные свойства, специфичные для их типа. Например, объект table имеет такие дополнительные свойства:

  • table.rows – коллекция строк <tr> таблицы;
  • table.caption/tHead/tFoot – ссылки на элементы таблицы <caption>, <thead>, <tfoot>;
  • table.tBodies – коллекция элементов таблицы <tbody> (по спецификации их может быть больше одного).

Объекты tr имеют такие свойства:

  • tr.cells – коллекция <td> и <th> ячеек, находящихся внутри строки <tr>;
  • tr.sectionRowIndex – номер строки <tr> в текущей секции <thead>/<tbody>/<tfoot>;
  • tr.rowIndex – номер строки <tr> в таблице (включая все строки таблицы).

И объект td:

  • td.cellIndex – номер ячейки в строке <tr>.

Они используются также как и описанные выше другие свойства узлов DOM-дерева. Например, если у нас имеется вот такая таблица:

<table id="table_digs">
  <tr>
    <td>один</td><td>два</td>
  </tr>
</table>

То можно обратиться к ячейке со строкой два и вывести ее содержимое в консоль:

console.log( table_digs.rows[0].cells[1].innerHTML );

Здесь свойство innerHTML содержит HTML-текст выбранной ячейки таблицы. О нем речь пойдет позже в других занятиях.

Видео по теме