en.javascript.info/2-ui/1-document/4-traversing-dom/article.md
2015-02-08 10:03:24 +03:00

16 KiB
Raw Blame History

Навигация по DOM-элементам

DOM позволяет делать что угодно с HTML-элементом и его содержимым, но для этого нужно сначала нужный элемент получить.

Доступ к DOM начинается с объекта document. Из него можно добраться до любых узлов.

[cut]

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

Посмотрим на них повнимательнее.

Сверху documentElement и body

Самые верхние элементы дерева доступны напрямую из document.

`<HTML>` = `document.documentElement`
Первая точка входа -- `document.documentElement`. Это свойство ссылается на DOM-объект для тега `<html>`.
`` = `document.body`
Вторая точка входа -- `document.body`, который соответствует тегу ``.

В современных браузерах (кроме IE8-) также есть document.head -- прямая ссылка на <head>

[warn header="Есть одна тонкость: document.body может быть равен null"] Нельзя получить доступ к элементу, которого еще не существует в момент выполнения скрипта.

В частности, если скрипт находится в <head>, то в нём недоступен document.body.

Поэтому в следующем примере первый alert выведет null:

<!--+ run -->
<!DOCTYPE HTML>
<html>
  <head>
    <script>
*!*
      alert("Из HEAD: " + document.body); // null, body ещё нет
*/!*
    </script>
  </head>
  <body>
  
    <script>
      alert("Из BODY: " + document.body); // body есть
    </script>

  </body>
</html>

[/warn]

[smart header="В DOM активно используется null"] В мире DOM в качестве значения, обозначающего "нет такого элемента" или "узел не найден", используется не undefined, а null. [/smart]

Дети: childNodes, firstChild, lastChild

Здесь и далее мы будем использовать два принципиально разных термина.

  • **Дочерние элементы (или дети)** -- элементы, которые лежат *непосредственно* внутри данного. Например, внутри `<HTML>` обычно лежат `<HEAD>` и ``.
  • **Потомки** -- все элементы, которые лежат внутри данного, вместе с их детьми, детьми их детей и так далее. То есть, всё поддерево DOM.

Псевдо-массив childNodes хранит все дочерние элементы, включая текстовые.

Пример ниже последовательно выведет дочерние элементы document.body:

<!--+ run -->
<!DOCTYPE HTML>
<html>
<body>
  <div>Начало</div>

  <ul>
    <li>Информация</li>
  </ul>

  <div>Конец</div>

  <script>
*!* 
    for(var i=0; i<document.body.childNodes.length; i++) {
      alert( document.body.childNodes[i] ); // Text, DIV, Text, UL, ..., SCRIPT
    }
*/!*
  </script>
  ...
</body>
</html>

Обратим внимание на маленькую деталь. Если запустить пример выше, то последним будет выведен элемент <script>. На самом-то деле в документе есть ещё текст (обозначенный троеточием), но на момент выполнения скрипта браузер ещё до него не дошёл.

Пробельный узел будет в итоговом документе, но его еще нет на момент выполнения скрипта.

[warn header="Список детей -- только для чтения!"] Скажем больше -- все навигационные свойства, которые перечислены в этой главе -- только для чтения. Нельзя просто заменить элемент присвоением childNodes[i] = ....

Изменение DOM осуществляется другими методами, которые мы рассмотрим далее, все навигационные ссылки при этом обновляются автоматически. [/warn]

Свойства firstChild и lastChild обеспечивают быстрый доступ к первому и последнему элементу.

Всегда верно:

elem.childNodes[0] === elem.firstChild
elem.childNodes[elem.childNodes.length - 1] === elem.lastChild

Коллекции -- не массивы

DOM-коллекции, такие как childNodes и другие, которые мы увидим далее, не являются JavaScript-массивами.

В них нет методов массивов, таких как forEach, map, push, pop и других.

//+ run
var elems = document.documentElement.childNodes;

*!*
elems.forEach(function(elem) { // нет такого метода!
*/!*
  /* ... */
});

Именно поэтому childNodes и называют "коллекция" или "псевдомассив".

Можно для перебора коллекции использовать обычный цикл for(var i=0; i<elems.length; i++) ... Но что делать, если уж очень хочется воспользоваться методами массива?

Это возможно, основных варианта два:

  1. Применить метод массива через `call/apply`:
    //+ run
    var elems = document.documentElement.childNodes;
    
    *!*
    [].forEach.call(elems, function(elem) { 
    */!*
      alert(elem);  // HEAD, текст, BODY
    });
    
  2. При помощи [Array.prototype.slice](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice) сделать из коллекции массив.

    Обычно вызов arr.slice(a, b) делает новый массив и копирует туда элементы arr с индексами от a до b-1 включительно. Если же вызвать его без аргументов arr.slice(), то он делает новый массив и копирует туда все элементы arr.

    Это работает и для коллекции:

    //+ run
    var elems = document.documentElement.childNodes;
    *!*
    elems = Array.prototype.slice.call(elems); // теперь elems - массив
    */!*
    
    elems.forEach(function(elem) { 
      alert(elem.tagName);  // HEAD, текст, BODY
    });
    

[warn header="Нельзя перебирать коллекцию через for..in"] Ранее мы говорили, что не рекомендуется использовать для перебора массива цикл for..in.

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

К примеру, код ниже должен перебрать все дочерние элементы <html>. Их, естественно, два: <head> и <body>. Максимум, три, если взять ещё и текст между ними.

Но в примере ниже alert сработает не три, а целых 5 раз!

//+ run
var elems = document.documentElement.childNodes;

for(var key in elems) {
  alert(key); // 0, 1, 2, length, item
}

Цикл for..in выведет не только ожидаемые индексы 0, 1, 2, по которым лежат узлы в коллекции, но и свойство length (в коллекции оно enumerable), а также функцию item(n) -- она никогда не используется, возвращает n-й элемент коллекции, проще обратиться по индексу [n].

В реальном коде нам нужны только элементы, мы же будем работать с ними, а служебные свойства -- не нужны. Поэтому желательно использовать for(var i=0; i<elems.length; i++). [/warn]

Соседи и родитель

Доступ к элементам слева и справа данного можно получить по ссылкам previousSibling / nextSibling.

Родитель доступен через parentNode. Если долго идти от одного элемента к другому, то рано или поздно дойдёшь до корня DOM, то есть до document.documentElement, а затем и document.

Навигация только по элементам

Навигационные ссылки, описанные выше, равно касаются всех узлов в документе. В частности, в childNodes сосуществуют и текстовые узлы и узлы-элементы и узлы-комментарии, если есть.

Но для большинства задач текстовые узлы нам не интересны.

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

Эти ссылки похожи на те, что раньше, только в ряде мест стоит слово Element:

  • `children` -- только дочерние узлы-элементы, то есть соответствующие тегам.
  • `firstElementChild`, `lastElementChild` -- соответственно, первый и последний дети-элементы.
  • `previousElementSibling`, `nextElementSibling` -- соседи-элементы.
  • `parentElement` -- родитель-элемент.

[smart header="Родитель-элемент? А что, бывают родители не-элементы?"] Почти всегда родитель и так элемент, ведь текстовые узлы не могут иметь потомков. Поэтому это свойство равно parentNode, кроме одного исключения.

В корне DOM находится document, и он-то как раз не элемент. То есть, если идти по цепочке parentElement вверх, то мы, остановимся мы не на document как в случае с parentNode, а на document.documentElement.

Иногда это имеет значение, если хочется перебрать всех предков и вызвать какой-то метод, а на документе его нет. [/smart]

Модифицируем предыдущий пример, применив children вместо childNodes.

Теперь он будет выводить не все узлы, а только узлы-элементы:

<!--+ run -->
<!DOCTYPE HTML>
<html>
<body>
  <div>Начало</div>

  <ul>
    <li>Информация</li>
  </ul>

  <div>Конец</div>

  <script>
*!* 
    for(var i=0; i<document.body.children.length; i++) {
      alert( document.body.children[i] ); // DIV, UL, DIV, SCRIPT
    }
*/!*
  </script>
  ...
</body>
</html>

Всегда верны равенства:

elem.firstElementChild === elem.children[0]
elem.lastElementChild === body.children[body.children.length-1]

[warn header="В IE8- поддерживается только children"] Других навигационных свойств в этих браузерах нет. Впрочем, как мы увидим далее, можно легко сделать полифилл, и они, всё же, будут. [/warn]

[warn header="В IE8- в children присутствуют узлы-комментарии"] С точки зрения стандарта это ошибка, но IE8- также включает в children узлы, соответствующие HTML-комментариям.

Это может привести к сюрпризам при использовании свойства children, поэтому HTML-комментарии либо убирают либо используют фреймворк, к примеру, jQuery, который даёт свои методы перебора и отфильтрует их. [/warn]

Особые ссылки для таблиц [#dom-navigation-tables]

У конкретных элементов DOM могут быть свои дополнительные ссылки для большего удобства навигации.

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

В списке ниже выделены наиболее полезные:

`TABLE`
  • **`table.rows`** -- список строк `TR` таблицы.
  • `table.caption/tHead/tFoot` -- ссылки на элементы таблицы `CAPTION`, `THEAD`, `TFOOT`.
  • `table.tBodies` -- список элементов таблицы `TBODY`, по спецификации их может быть несколько.
`THEAD/TFOOT/TBODY`
  • `tbody.rows` -- список строк `TR` секции.
`TR`
  • **`tr.cells`** -- список ячеек `TD/TH`
  • **`tr.sectionRowIndex`** -- номер строки в текущей секции `THEAD/TBODY`
  • `tr.rowIndex` -- номер строки в таблице
`TD/TH`
  • **`td.cellIndex`** -- номер ячейки в строке

Пример использования:

<!--+ run height=100 -->
<table>
  <tr> 
    <td>один</td> <td>два</td>    
  </tr>
  <tr> 
    <td>три</td>  <td>четыре</td> 
  </tr>
</table>

<script>
var table = document.body.children[0];

alert( table.*!*rows[0].cells[0]*/!*.innerHTML ) // "один"
</script>

Спецификация: HTML5: tabular data.

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

Конечно же, таблицы -- не исключение.

Аналогичные полезные свойства есть у HTML-форм, они позволяют из формы получить все её элементы, а из них -- в свою очередь, форму. Мы рассмотрим их позже.

Итого

В DOM доступна навигация по соседним узлам через ссылки:

  • По любым узлам.
  • Только по элементам.

Также некоторые виды элементов предоставляют дополнительные ссылки для большего удобства, например у таблиц есть свойства для доступа к строкам/ячейкам.

[libs] d3 domtree [/libs]