13. Document Object Model

Когда вы открываете веб-страницу в браузере, он получает исходный текст HTML и разбирает (парсит) его примерно так, как наш парсер из главы 11 разбирал программу. Браузер строит модель структуры документа и использует её, чтобы нарисовать страницу на экране.

Это представление документа и есть одна из игрушек, доступных в песочнице JavaScript. Вы можете читать её и изменять. Она изменяется в реальном времени – как только вы её подправляете, страница на экране обновляется, отражая изменения.

Структура документа

Можно представить HTML как набор вложенных коробок. Теги вроде

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

 

  Моя домашняя страничка

 

 

  

Моя домашняя страничка

  

Привет, я Марейн и это моя домашняя страничка.

  

А ещё я книжку написал! Читайте её

   здесь.

 

У этой страницы следующая структура:



Структура данных, использующаяся браузером для представления документа, отражает его форму. Для каждой коробки есть объект, с которым мы можем взаимодействовать и узнавать про него разные данные – какой тег он представляет, какие коробки и текст содержит. Это представление называется Document Object Model (объектная модель документа), или сокращённо DOM.

Мы можем получить доступ к этим объектам через глобальную переменную

document
. Её свойство
documentElement
ссылается на объект, представляющий тег . Он также предоставляет свойства
head
и
body
, в которых содержатся объекты для соответствующих элементов.

Деревья

Вспомните синтаксические деревья из главы 11. Их структура удивительно похожа на структуру документа браузера. Каждый узел может ссылаться на другие узлы, у каждого из ответвлений может быть своё ответвление. Эта структура – типичный пример вложенных структур, где элементы содержат подэлементы, похожие на них самих.

Мы зовём структуру данных деревом, когда она разветвляется, не имеет циклов (узел не может содержать сам себя), и имеет единственный ярко выраженный «корень». В случае DOM в качестве корня выступает document.documentElement.

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

У типичного дерева есть разные узлы. У синтаксического дерева языка Egg были переменные, значения и приложения. У приложений всегда были дочерние ветви, а переменные и значения были «листьями», то есть узлами без дочерних ответвлений.

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

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

У каждого узлового объекта DOM есть свойство

nodeType
, содержащее цифровой код, определяющий тип узла. У обычных элементов он равен 1, что также определено в виде свойства-константы
document.ELEMENT_NODE
. У текстовых узлов, представляющих отрывки текста, он равен 3 (
document.TEXT_NODE
). У комментариев — 8 (
document.COMMENT_NODE
).

То есть, вот ещё один способ графически представить дерево документа:



Листья – текстовые узлы, а стрелки показывают взаимоотношения отец-ребёнок между узлами.

Стандарт

Использовать загадочные цифры для представления типа узла – это подход не в стиле JavaScript. Позже мы встретимся с другими частями интерфейса DOM, которые тоже кажутся чуждыми и нескладными. Причина в том, что DOM разрабатывался не только для JavaScript. Он пытается определить интерфейс, не зависящий от языка, который можно использовать и в других системах – не только в HTML, но и в XML, который представляет из себя формат данных общего назначения с синтаксисом, напоминающим HTML.

Получается неудобно. Хотя стандарты – и весьма полезная штука, в нашем случае преимущество независимости от языка не такое уж и полезное. Лучше иметь интерфейс, хорошо приспособленный к языку, который вы используете, чем интерфейс, который будет знаком при использовании разных языков.

Чтобы показать неудобную интеграцию с языком, рассмотрим свойство

childNodes
, которое есть у узлов DOM. В нём содержится объект, похожий на массив, со свойством
length
, и пронумерованные свойства для доступа к дочерним узлам. Но это – экземпляр типа
NodeList
, не настоящий массив, поэтому у него нет методов вроде
forEach
.

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

Но эти проблемы не фатальные. JavaScript позволяет создавать абстракции. Легко написать вспомогательные функции, позволяющие выражать операции более понятно и коротко. Вообще, такого рода инструменты предоставляют много библиотек, направленных на программирование для браузера.

Обход дерева

Узлы DOM содержат много ссылок на соседние. Это показано на диаграмме:



Хотя тут показано только по одной ссылке каждого типа, у каждого узла есть свойство

parentNode
, указывающего на его родительский узел. Также у каждого узла-элемента (тип 1) есть свойство
childNodes
, указывающее на массивоподобный объект, содержащий его дочерние узлы.

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

firstChild
и
lastChild
показывают на первый и последний дочерний элементы, или содержат
null
у тех узлов, у которых нет дочерних.
previousSibling
и
nextSibling
указывают на соседние узлы – узлы того же родителя, что и текущего узла, но находящиеся в списке сразу до или после текущей. У первого узла свойство
previousSibling
будет
null
, а у последнего
nextSibling
будет
null
.

При работе с такими вложенными структурами пригождаются рекурсивные функции. Следующая ищет в документе текстовые узлы, содержащие заданную строку, и возвращает

true
, когда находит:

function talksAbout(node, string) {

 if (node.nodeType == document.ELEMENT_NODE) {

  for (var i = 0; i < node.childNodes.length; i++) {

   if (talksAbout(node.childNodes[i], string))

    return true;

  }

  return false;

 } else if (node.nodeType == document.TEXT_NODE) {

  return node.nodeValue.indexOf(string) > -1;

 }

}


console.log(talksAbout(document.body, "книг"));

// → true

Свойства текстового узла

nodeValue
содержит строчку текста.

Поиск элементов

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

document.body
и тупо перебирая жёстко заданный в коде путь. Поступая так, мы вносим в программу допущения о точной структуре документа – а её мы позже можем захотеть поменять. Другой усложняющий фактор – текстовые узлы создаются даже для пробелов между узлами. В документе из примера у тега
body
не три дочерних (
h1
и два
p
), а целых семь: эти три плюс пробелы до, после и между ними.

Так что если нам нужен атрибут

href
из ссылки, мы не должны писать в программе что-то вроде: «второй ребёнок шестого ребёнка document.body». Лучше бы, если б мы могли сказать: «первая ссылка в документе». И так можно сделать:

var link = document.body.getElementsByTagName("a")[0];

console.log(link.href);

У всех узлов-элементов есть метод

getElementsByTagName
, собирающий все элементы с данным тегом, которые происходят (прямые или не прямые потомки) от этого узла, и возвращает его в виде массивоподобного объекта.

Чтобы найти конкретный узел, можно задать ему атрибут

id
и использовать метод
document.getElementById
.

Мой страус Гертруда:


Третий метод –

getElementsByClassName
, который, как и
getElementsByTagName
, ищет в содержимом узла-элемента и возвращает все элементы, содержащие в своём классе заданную строчку.

Меняем документ

Почти всё в структуре DOM можно менять. У узлов-элементов есть набор методов, которые используются для их изменения. Метод

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

Один

Два

Три


Узел может существовать в документе только в одном месте. Поэтому вставляя параграф «Три» перед параграфом «Один» мы фактически удаляем его из конца списка и вставляем в начало, и получаем «Три/Один/Два». Все операции по вставке узла приведут к его исчезновению с текущей позиции (если у него таковая была).

Метод

replaceChild
используется для замены одного дочернего узла другим. Он принимает два узла: новый, и тот, который надо заменить. Заменяемый узел должен быть дочерним узлом того элемента, чей метод мы вызываем. Как
replaceChild
, так и
insertBefore
в качестве первого аргумента ожидают получить новый узел.

Создание узлов

В следующем примере нам надо сделать скрипт, заменяющий все картинки (тег

) в документе текстом, содержащимся в их атрибуте
alt
, который задаёт альтернативное текстовое представление картинки.

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

document.createTextNode
.

Это Кошка в

 сапожках.



Получая строку,

createTextNode
даёт нам тип 3 узла DOM (текстовый), который мы можем вставить в документ, чтобы он был показан на экране.

Цикл по картинкам начинается в конце списка узлов. Это сделано потому, что список узлов, возвращаемый методом

getElementsByTagName
(или свойством
childNodes
) постоянно обновляется при изменениях документа. Если б мы начали с начала, удаление первой картинки привело бы к потере списком первого элемента, и во время второго прохода цикла, когда
i
равно 1, он бы остановился, потому что длина списка стала бы также равняться 1.

Если вам нужно работать с фиксированным списком узлов вместо «живого», можно преобразовать его в настоящий массив при помощи метода

slice
.

var arrayish = {0: "один", 1: "два", length: 2};

var real = Array.prototype.slice.call(arrayish, 0);

real.forEach(function(elt) { console.log(elt); });

// → один

//  два

Для создания узлов-элементов (тип 1) можно использовать

document.createElement
. Метод принимает имя тега и возвращает новый пустой узел заданного типа. Следующий пример определяет инструмент
elt
, создающий узел-элемент и использующий остальные аргументы в качестве его детей. Эта функция потом используется для добавления дополнительной информации к цитате.

Никакая книга не может быть закончена. Во время работы над ней мы узнаём достаточно для того, чтобы найти её незрелой сразу же после того, как мы отвлеклись от неё.


Атрибуты

К некоторым элементам атрибутов, типа

href
у ссылок, можно получить доступ через одноимённое свойство объекта. Это возможно для ограниченного числа часто используемых стандартных атрибутов.

Но HTML позволяет назначать узлам любые атрибуты. Это полезно, т. к. позволяет вам хранить дополнительную информацию в документе. Если вы придумаете свои названия атрибутов, их не будет среди свойств узла-элемента. Вместо этого вам надо будет использовать методы

getAttribute
и
setAttribute
для работы с ними.

Код запуска 00000000.

У кошки четыре ноги.


Рекомендую перед именами придуманных атрибутов ставить

data-
, чтобы быть уверенным, что они не конфликтуют с любыми другими. В качестве простого примера мы напишем подсветку синтаксиса, который ищет теги
(“preformatted”, предварительно отформатированный – используется для кода и простого текста) с атрибутом
data-language
(язык) и довольно грубо пытается подсветить ключевые слова в языке.

function highlightCode(node, keywords) {

 var text = node.textContent;

 node.textContent = ""; // Очистим узел


 var match, pos = 0;

 while (match = keywords.exec(text)) {

  var before = text.slice(pos, match.index);

  node.appendChild(document.createTextNode(before));

  var strong = document.createElement("strong");

  strong.appendChild(document.createTextNode(match[0]));

  node.appendChild(strong);

  pos = keywords.lastIndex;

 }

 var after = text.slice(pos);

 node.appendChild(document.createTextNode(after));

}

Функция

highlightCode
принимает узел
и регулярку (с включённой настройкой global), совпадающую с ключевым словом языка программирования, которое содержит элемент.

Свойство

textContent
используется для получения всего текста узла, а затем устанавливается в пустую строку, что приводит к очищению узла. Мы в цикле проходим по всем вхождениям выражения keyword, добавляем между ними текст в виде простых текстовых узлов, а совпавший текст (ключевые слова) добавляем, заключая их в элементы
(жирный шрифт).

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

, у которых есть атрибут
data-language
, и вызывая на каждом
highlightCodeс
правильной регуляркой.

var languages = {

 javascript: /\b(function|return|var)\b/g /* … etc */

};


function highlightAllCode() {

 var pres = document.body.getElementsByTagName("pre");

 for (var i = 0; i < pres.length; i++) {

  var pre = pres[i];

  var lang = pre.getAttribute("data-language");

  if (languages.hasOwnProperty(lang))

   highlightCode(pre, languages[lang]);

 }

}

Вот пример:

А вот и она, функция идентификации:

function id(x) { return x; }

Есть один часто используемый атрибут,

class
, имя которого является ключевым словом в JavaScript. По историческим причинам, когда старые реализации JavaScript не умели обращаться с именами свойств, совпадавшими с ключевыми словами, этот атрибут доступен через свойство под названием
className
. Вы также можете получить к нему доступ по его настоящему имени
class
через методы
getAttribute
и
setAttribute
.

Расположение элементов (layout)

Вы могли заметить, что разные типы элементов располагаются по-разному. Некоторые, типа параграфов

и заголовков

растягиваются на всю ширину документа и появляются на отдельных строках. Такие элементы называют блочными. Другие, как ссылки
или жирный текст
появляются на одной строчке с окружающим их текстом. Они называются встроенными (inline).

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

Размер и положение элемента можно узнать через JavaScript. Свойства

offsetWidth
и
offsetHeight
выдают размер в пикселях, занимаемый элементом. Пиксель – основная единица измерений в браузерах, и обычно соответствует размеру минимальной точки экрана. Сходным образом,
clientWidth
и
clientHeight
дают размер внутренней части элемента, не включая ширину его границ (border).

 Я в коробочке


Самый эффективный способ узнать точное расположение элемента на экране – метод

getBoundingClientRect
. Он возвращает объект со свойствами
top
,
bottom
,
left
, и
right
(сверху, снизу, слева и справа), которые содержат положение элемента относительно левого верхнего угла экрана в пикселях. Если вам надо получить эти данные относительно всего документа, вам надо прибавить текущую позицию прокрутки, которая содержится в глобальных переменных
pageXOffset
и
pageYOffset
.

Разбор документа – задача сложная. В целях быстродействия браузерные движки не перестраивают документ каждый раз после его изменения, а ждут так долго, как это возможно. Когда программа JavaScript, изменившая документ, заканчивает работу, браузеру надо будет просчитать новую раскладку страницы, чтобы вывести изменённый документ на экран. Когда программа запрашивает позицию или размер чего-либо, читая свойства типа

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

Программа, которая периодически считывает раскладку DOM и изменяет DOM, заставляет браузер много раз пересчитывать раскладку, и в связи с этим будет работать медленно. В следующем примере есть две разные программы, которые строят линию из символов X шириной в 2000 пикселей, и измеряют время работы.


Стили

Мы видели, что разные элементы HTML ведут себя по-разному. Некоторые показываются в виде блоков, другие встроенные. Некоторые добавляют визуальный стиль – например,

делает жирным текст и
делает текст подчёркнутым и синим.

Внешний вид картинки в теге

или то, что ссылка в теге
при клике открывает новую страницу, связано с типом элемента. Но основные стили, связанные с элементом, вроде цвета текста или подчёркивания, могут быть нами изменены. Вот пример использования свойства
style
(стиль):

Обычная ссылка

Зелёная ссылка

Атрибут

style
может содержать одно или несколько объявлений свойств (
color
), за которым следует двоеточие и значение. В случае нескольких объявлений они разделяются точкой с запятой:
“color: red; border: none”
.

Много всякого можно изменить при помощи стилей. Например, свойство

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

Текст показан встроенным,

в виде блока, и

вообще не виден.

Блочный элемент выводится отдельным блоком, а последний вообще не виден –

display: none
отключает показ элементов. Таким образом можно прятать элементы. Обычно это предпочтительно полному удалению их из документа, потому что их легче потом при необходимости снова показать.

Код JavaScript может напрямую действовать на стиль элемента через свойство узла

style
. В нём содержится объект, имеющий свойства для всех свойств стилей. Их значения – строки, в которые мы можем писать для смены какого-то аспекта стиля элемента.

 Красотень


Некоторые имена свойств стилей содержат дефисы, например

font-family
. Так как с ними неудобно было бы работать в JavaScript (пришлось бы писать
style[«font-family»]
), названия свойств в объекте стилей пишутся без дефиса, а вместо этого в них появляются прописные буквы:
style.fontFamily
.

Каскадные стили

Система стилей в HTML называется CSS (Cascading Style Sheets, каскадные таблицы стилей). Таблица стилей – набор стилей в документе. Его можно писать внутри тега

Теперь текст тега strong наклонный и серый.

«Каскадные» означает, что несколько правил комбинируются для получения окончательного стиля документа. В примере на стиль по умолчанию для

, который делает текст жирным, накладывается правило из тега


Элементы по имени тегов

Метод

getElementsByTagName
возвращает все дочерние элементы с заданным именем тега. Сделайте свою версию этого метода в виде обычной функции, которая принимает узел и строчку (имя тега) и возвращает массив, содержащий все нисходящие узлы с заданным именем тега.

Чтобы выяснить имя тега элемента, используйте свойство

tagName
. Заметьте, что оно возвратит имя тега в верхнем регистре. Используйте методы строк
toLowerCase
или
toUpperCase
.

Заголовок с элементом span внутри.

Параграф с раз, два элементами spans.


Шляпа кота

Расширьте анимацию кота, чтобы и кот и его шляпа

летали по противоположным сторонам эллипса.

Или пусть шляпа летает вокруг кота. Или ещё что-нибудь интересное придумайте.

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

top
и
left
будут считаться относительно левого верхнего угла документа. Чтобы не использовать отрицательные координаты, вы можете добавить заданное число пикселей к значениям
position
.


Загрузка...