Объектная модель документа, или сокращенно DOM, служит ориентиром для браузера при размещении элементов на веб-странице. Места, где элементы размещаются в DOM, называются Nodes, и на веб-странице не только элементы HTML получают свои узлы, но и атрибуты элементов HTML имеют свои узлы (attribute nodes), каждый фрагмент текста имеет его узел (text nodes), и есть много других типов узлов. Структурное отношение этих узлов отражает структуру HTML-документа. Благодаря этому мы можем определить отношения между элементами на странице как отношения между их узлами в DOM.

Когда мы манипулируем элементами на веб-странице с помощью языка программирования, такого как JavaScript, мы делаем это через их узлы DOM. Получая доступ к узлу DOM данного элемента, мы можем манипулировать его свойствами, такими как положение, внешний вид, содержимое, поведение и т. Д. Часто мы хотим выполнять действия с элементами, которые имеют между собой какие-то отношения, известные как связанные узлы. Для этого у нас должен быть способ перехода от одного узла к другому, то есть способ обхода DOM.

Имея доступ к определенному узлу в DOM, есть способы пройти через DOM, используя связанные с ним узлы. Мы можем двигаться вверх или вниз по дереву DOM, или мы можем двигаться в сторону, оставаясь на том же уровне DOM. В этой статье мы рассмотрим, как получить доступ к связанным узлам DOM с помощью языка программирования JavaScript.

Отношения между узлами DOM

Во-первых, давайте посмотрим, какие отношения существуют между элементами, чтобы позже мы могли лучше понять методы, используемые для доступа к ним. Элементы HTML вложены друг в друга, образуя древовидную структуру. Может быть много уровней вложенности элементов, и все это отражается в дереве DOM узлов элементов.

Элементы потомков и предков

Это отношение бесполезно, но поможет нам более четко выразить отношения между элементами.

Один элемент может иметь много уровней других элементов, вложенных под ним, и все эти вложенные элементы на всех уровнях вложенности называются дочерними элементами нашего начального элемента. Например, у нас есть элемент main для основного содержания страницы со следующим содержанием:

На первом уровне вложенности есть <h1> элемент и два элемента ‹article›. Затем на втором уровне вложенности у нас есть <h2> и <section> элементов, и, наконец, на третьем уровне есть <p> элементов внутри <section> элементов. Все эти элементы являются потомками элемента <main>.

С учетом сказанного, элемент <main> является их предком, элементом, которому они принадлежат в дереве DOM.

Но отношения потомков / предков можно увидеть и между другими элементами в этом примере. Например, элементы <article> являются предками для своих вложенных элементов <h2>, <section> и <p>, а эти элементы, в свою очередь, являются его дочерними элементами. То же самое относится к элементам <section> и <p>.

Здесь мы должны указать на одну важную вещь. Например, заголовок «Как вырастить бонсай» не является потомком <article id=”article-1">, а также этот товар не является прародителем указанного заголовка. Причина этого в том, что заголовок «Как вырастить бонсай» вложен не в первую статью, а во вторую статью. Следовательно, между ними нет отношений потомков / предков. То же самое относится к элементам <section> и <p>, они являются потомками элемента article, в который они вложены, и этот элемент article является их предком.

Родительские и дочерние элементы

Особые и очень полезные отношения потомок / предок - это случай, когда элементы являются прямыми потомками или предками данного узла. «Прямой» означает, что они находятся всего в одном уровне вложенности от данного узла.

родительский узел (элемент) - это ближайший элемент-предок данного элемента. Если мы выберем заголовок «Как вырастить бонсай», первым элементом-предком (на один уровень выше) будет элемент <article id=”article-2">, и мы будем называть его родительским элементом данного заголовка. У элементов <article> один и тот же родительский элемент, они оба вложены в элемент <main>, который является их родительским элементом. Обратите внимание, что <h1> также имеет родительский элемент <main>. Абзац «Что нам теперь делать !?» имеет в качестве своего родителя элемент <section> под первым элементом article.

Напротив родительского элемента находится дочерний элемент, но хотя этот элемент может иметь только один родительский элемент, под ним может быть много дочерних элементов. Дочерние элементы - это все прямые дочерние элементы (на один уровень ниже) данного элемента. Дочерние элементы элемента <main> - это <h1> и оба <article> элемента и никакие другие элементы. Дочерние элементы второй статьи - это заголовок «Как вырастить бонсай» и вложенный под ним <section>. Абзац в этом разделе не является дочерним элементом второго элемента article, это дочерний элемент элемента <section>.

Еще одна важная вещь, на которую следует обратить внимание, - это то, что каждый бит текста в HTML представлен текстовым узлом в модели DOM. Учитывая вышесказанное, заголовок в первой статье имеет один текстовый узел в качестве своего дочернего узла, узел, содержащий текст «Первый контакт с инопланетными существами в истории человечества», а элемент заголовка является родительским для этого текстового узла, а заголовок во второй статье - родительский элемент дочернего текстового узла с текстом «Как вырастить бонсай».

Родственные элементы

Два или более элемента являются братьями и сестрами, если они имеют тот же элемент, что и их родительский элемент. В нашем примере <h1> и оба <article> элемента являются братьями и сестрами, потому что у них один и тот же родительский элемент - элемент <main>. Элементы <p> в первой статье являются родственными, потому что их родительский элемент - <section> в первой статье. Но элемент <p> во второй статье не является родственником элементов <p> в первой статье, потому что не все они имеют одного и того же родителя, даже если они находятся на одном уровне вложенности.

Обход DOM через связанные узлы

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

Мы добавим некоторые идентификаторы и классы в наш пример HTML, чтобы мы могли лучше получить доступ к элементам в дереве DOM:

Поиск родительского узла данного узла

Если у нас есть объект Node, который является ссылкой на node в DOM, чтобы получить его родительский узел, мы можем использовать свойство parentNode в node. Поскольку node - объект, а parentNode - свойство, мы можем использовать «точечную» нотацию для доступа к родительскому элементу node следующим образом:

const parent = node.parentNode;

Давайте найдем родительский узел первого узла article в нашем примере HTML.

Теперь давайте найдем родительский элемент заголовка «Как вырастить бонсай».

Мы можем использовать родительский узел данного узла, чтобы получить всех предков данного узла в дереве DOM. Например:

Узел grandParent также можно получить, связав родительский узел property с узлом bonsai:

const grandParent = bonsai.parentNode.parentNode;

parentNode часто используется для удаления заданного узла из DOM. Допустим, мы хотим удалить первую статью из документа, так как это слишком неприятно. Мы бы сделали это так:

Родительский элемент HTML-элемента может быть узлом Element, узлом Document или узлом DocumentFragment. Свойство parentNode узла может возвращать null в тех случаях, когда оно применяется к узлам Document и DocumentFragment, потому что у них никогда не может быть родительских узлов. Если узел только что создан, но не прикреплен к DOM, применение parenNode к нему также вернет null.

Еще одна вещь, на которую следует обратить внимание, это то, что свойство parentNode доступно только для чтения, а это означает, что невозможно сделать что-то вроде этого:

Поиск дочерних узлов данного узла

Чтобы получить все дочерние узлы node, мы можем использовать его свойство childNodes. Например:

const children = node.childNodes;

Результатом является список узлов, список объектов, каждый из которых представляет один дочерний узел нашего node. Список узлов похож на массивы в том смысле, что можно перемещаться по списку, используя индексы массивов. Например, напечатайте свойство nodeName всех дочерних элементов в списке узлов.

Давайте воспользуемся другим HTML-кодом, чтобы объяснить некоторые важные последствия использования свойства childNodes:

Если мы применим предыдущий код javascript для распечатки имен дочерних узлов из элемента <ul>, мы получим такой результат:

Не совсем так, как можно было ожидать. Вы могли подумать, что будет напечатано только шесть li имен элементов, но вместо этого мы получим еще семь текстовых узлов.

Причина этого в том, что список узлов также содержит текстовые узлы и узлы комментариев, поэтому мы должны проявлять осторожность при использовании этого свойства. Это происходит потому, что в HTML новая строка рассматривается как пробел, то есть как текстовый узел. Таким образом, разрывы строк и пробелы между тегами HTML добавляют текстовые узлы к списку дочерних узлов.

Чтобы избежать этого, мы могли бы реорганизовать нашу HTML-структуру следующим образом:

Теперь мы получаем ожидаемый результат:

Список узлов - это живой список узлов. Это означает, что добавление или удаление дочерних элементов в node приведет к обновлению списка, нам не нужно получать его снова. Это также означает, что при изменении количества элементов в списке узлов цикл for, используемый таким образом, завершится ошибкой:

Этот код приведет к ошибке на шестой итерации: TypeError: children[i] is undefined. Это потому, что список childrennode изменился с удалением третьего элемента. Чтобы избежать этого, мы должны обновлять переменную len на каждой итерации или, лучше, использовать children.length в условиях цикла вместо len, как мы это делали.

Если мы хотим получить только HTML-элементы в качестве дочерних для node, мы можем использовать свойство children вместо свойства childNodes:

const childElements = list.children;

"Особые" дети

Что ж, часто в семьях среди детей есть любимчики, «особенные». То же самое и с семейством узлов, есть особенные дети. Что это за дети? - спросите вы. И вы, наверное, догадались, что это первая и последняя. :)

Эта аналогия - не бессмысленная шутка. В дереве DOM узел с дочерними узлами имеет определенные свойства firstChild и lastChild. Они используются для быстрого поиска первого и последнего дочерних узлов данного узла. В нашем примере первый дочерний элемент будет соответствовать первому элементу списка, а последний дочерний элемент будет соответствовать шестому элементу списка. Вот как мы можем получить эти элементы:

Важно отметить, что firstChild и lastChild также обрабатывают разрывы строк как текстовые узлы, и в случае нашего первого списка (один с разрывами строк) они будут приводить к текстовым узлам, а не к элементам списка.

Поиск братьев и сестер узла

Когда у нас есть доступ к node, мы можем получить доступ к его родственным узлам, используя свойства nextSibling и previousSibling.

Свойство nextSibling получит родственный узел, следующий сразу за заданным node. Синтаксис следующий:

const next = node.nextSibling;

Давайте найдем следующего брата элемента с id=”three”:

Теперь мы можем приступить к пошаговому рассмотрению следующих братьев и сестер:

Когда мы дойдем до последнего брата в родительском узле, использование nextSibling вернет null, потому что после последнего дочернего узла больше нет братьев и сестер:

Свойство previousSibling получит родственный узел, который непосредственно предшествует заданному node. Синтаксис аналогичен синтаксису для nextSibling:

const previous = node.previousSibling;

Давайте найдем предыдущего брата элемента с id=”three”:

Теперь мы можем перейти к пошаговому рассмотрению других предыдущих братьев и сестер:

Когда мы дойдем до первого брата в родительском узле, использование previousSibling вернет null, потому что перед первым дочерним узлом нет братьев и сестер:

Заключительный пример

Теперь давайте рассмотрим один пример, который требует более сложного обхода, чем мы показывали до сих пор в наших предыдущих примерах. Давайте снова воспользуемся первой структурой примера HTML:

Допустим, у нас есть доступ к заголовку в первой статье (<h2 class=”sensations”>), и мы хотим прочитать текст заголовка из следующей статьи.

Ой, не тот узел! Помните, что разрывы строк обрабатываются как пробелы, а эти пробелы представлены текстовым узлом. Итак, как этого избежать?

Мы могли бы использовать цикл, который проходит через следующих братьев и сестер и проверяет, является ли узел узлом элемента, а не текстом или любым другим узлом. Чтобы проверить, является ли node узлом элемента, мы можем проверить свойство узла с именем nodeType. Свойство nodeType для узла элемента будет иметь значение ELEMENT_NODE, которое называется константой, или оно может иметь числовое значение 1, вы можете проверить любое из них. Чтобы увидеть все остальные типы узлов и их значения, прочтите эту статью.

Для этой задачи лучше всего использовать цикл 95_. Давайте изменим предыдущий код и добавим цикл while и проверку типа узла.

Теперь мы нашли следующую статью. Наконец, мы хотим поместить заголовок под ним. Поскольку мы знаем, что заголовок является первым узлом элемента под узлом статьи, мы могли бы подумать об использовании свойства firstChild на узле nextNode, но проблема с текстовыми узлами возникнет снова. Чтобы избежать этого, мы можем снова использовать цикл, чтобы проверить тип узла, или мы можем использовать querySelector на nextNode, чтобы получить первый элемент h2, и, вероятно, этот второй подход может быть самым безопасным в использовании. Но, ради практики, давайте воспользуемся свойством children объекта node, о котором мы кратко упоминали ранее.

Свойство children вернет узлы дочерних элементов списка, и тогда мы сможем просто выбрать первый из списка. Вот полный код:

Заключение

Присмотревшись к последнему примеру, можно спросить, зачем возиться со всем этим обходом связанных узлов, когда мы можем просто выполнить простой запрос вроде этого:

const secondH2 = document.querySelector('.horticulture');

Хотя это верно в этом простом случае, помните, что мы явно хотели перейти к заголовку следующей статьи, и что на реальном веб-сайте страница может быть намного больше, а содержимое может быть вложено гораздо глубже. . Кроме того, контент может быть сгенерирован автоматически, и мы не знаем, какие классы будут нам доступны. Итак, нам нужно будет делать полный запрос DOM для каждого следующего заголовка. Но, используя связанных братьев и сестер, мы можем находиться вокруг интересующих нас узлов и не запрашивать каждый раз все дерево DOM, тем самым повышая производительность, когда у нас очень большая веб-страница.

Чтобы эффективно использовать это на большой веб-странице со множеством статей, мы можем обернуть все это в функцию, которая принимает текущий узел в качестве параметра:

Обратите внимание, что мы добавили еще одну проверку nextNode.nodeName === ‘ARTICLE’ к условию цикла while, чтобы убедиться, что мы получаем правильный тип элемента, потому что может случиться так, что к элементам article есть другие родственники, которые не являются статьями.

Теперь мы можем вызвать функцию nextHeading для второго заголовка, чтобы попытаться получить заголовок из следующей статьи:

Есть много вариантов использования, когда вы можете использовать связанные узлы для обхода DOM. Какой метод вы будете использовать, зависит от структуры HTML и вашего воображения. Итак, познакомьтесь с семейством узлов и их отношениями, и они очень помогут вам с задачами, связанными с DOM.

Спасибо за чтение и получайте удовольствие от поиска в DOM!