Теперь давайте более внимательно взглянем на DOM-узлы.
В этой главе мы подробнее разберём, что они собой представляют и изучим их основные свойства.
Классы DOM-узлов
У разных DOM-узлов могут быть разные свойства. Например, у узла, соответствующего тегу <a>, есть свойства, связанные со ссылками, а у соответствующего тегу <input> – свойства, связанные с полем ввода и т.д. Текстовые узлы отличаются от узлов-элементов. Но у них есть общие свойства и методы, потому что все классы DOM-узлов образуют единую иерархию.
Каждый DOM-узел принадлежит соответствующему встроенному классу.
Корнем иерархии является EventTarget, от него наследует Node и остальные DOM-узлы.
На рисунке ниже изображены основные классы:
Существуют следующие классы:
-
EventTarget – это корневой «абстрактный» класс для всего.
Объекты этого класса никогда не создаются. Он служит основой, благодаря которой все DOM-узлы поддерживают так называемые «события», о которых мы поговорим позже.
-
Node – также является «абстрактным» классом, и служит основой для DOM-узлов.
Он обеспечивает базовую функциональность:
parentNode,nextSibling,childNodesи т.д. (это геттеры). Объекты классаNodeникогда не создаются. Но есть определённые классы узлов, которые наследуются от него (и следовательно наследуют функционалNode). -
Document, по историческим причинам часто наследуется
HTMLDocument(хотя последняя спецификация этого не навязывает) – это документ в целом.Глобальный объект
documentпринадлежит именно к этому классу. Он служит точкой входа в DOM. -
CharacterData – «абстрактный» класс. Вот, кем он наследуется:
-
Element – это базовый класс для DOM-элементов.
Он обеспечивает навигацию на уровне элементов:
nextElementSibling,children. А также и методы поиска элементов:getElementsByTagName,querySelector.Браузер поддерживает не только HTML, но также XML и SVG. Таким образом, класс
Elementслужит основой для более специфичных классов:SVGElement,XmlElement(они нам здесь не нужны) иHTMLElement. -
И наконец, HTMLElement является базовым классом для всех остальных HTML-элементов. Мы будем работать с ним большую часть времени.
От него наследуются конкретные элементы:
- HTMLInputElement – класс для тега
<input>, - HTMLBodyElement – класс для тега
<body>, - HTMLAnchorElement – класс для тега
<a>, - ...и т.д.
- HTMLInputElement – класс для тега
Также существует множество других тегов со своими собственными классами, которые могут иметь определенные свойства и методы, в то время как некоторые элементы, такие как <span>, <section> и <article>, не имеют каких-либо определенных свойств, поэтому они являются экземплярами класса HTMLElement.
Таким образом, полный набор свойств и методов данного узла является результатом цепочки наследования.
Рассмотрим DOM-объект для тега <input>. Он принадлежит классу HTMLInputElement.
Он получает свойства и методы из (в порядке наследования):
HTMLInputElement– этот класс предоставляет специфичные для элементов формы свойства,HTMLElement– предоставляет общие для HTML-элементов методы (и геттеры/сеттеры),Element– предоставляет типовые методы элемента,Node– предоставляет общие свойства DOM-узлов,EventTarget– обеспечивает поддержку событий (поговорим о них дальше),- ...и, наконец, он наследует от
Object, поэтому доступны также методы «обычного объекта», такие какhasOwnProperty.
Для того, чтобы узнать имя класса DOM-узла, вспомним, что обычно у объекта есть свойство constructor. Оно ссылается на конструктор класса, и в свойстве constructor.name содержится его имя:
...Или мы можем просто привести его к строке:
Проверить наследование можно также при помощи instanceof:
Как видно, DOM-узлы – это обычные JavaScript объекты. Для наследования они используют классы, основанные на прототипах.
В этом легко убедиться, если вывести в консоли браузера любой элемент через console.dir(elem). Или даже напрямую обратиться к методам, которые хранятся в HTMLElement.prototype, Element.prototype и т.д.
console.dir(elem) и console.log(elem)Большинство браузеров поддерживают в инструментах разработчика две команды: console.log и console.dir. Они выводят свои аргументы в консоль. Для JavaScript-объектов эти команды обычно выводят одно и то же.
Но для DOM-элементов они работают по-разному:
console.log(elem)выводит элемент в виде DOM-дерева.console.dir(elem)выводит элемент в виде DOM-объекта, что удобно для анализа его свойств.
Попробуйте сами на document.body. Вы увидите разницу во всех современных браузерах (кроме Firefox, где console.log(elem) и console.dir(elem) выводят одно и то же – элемент в виде DOM-объекта).
В спецификации для описания классов DOM используется не JavaScript, а специальный язык Interface description language (IDL), с которым достаточно легко разобраться.
В IDL все свойства представлены с указанием их типов. Например, DOMString, boolean и т.д.
Небольшой отрывок IDL с комментариями:
// Объявление HTMLInputElement
// Двоеточие ":" после HTMLInputElement означает, что он наследует от HTMLElement
interface HTMLInputElement: HTMLElement {
// далее идут свойства и методы элемента <input>
// "DOMString" означает, что значение свойства - строка
attribute DOMString accept;
attribute DOMString alt;
attribute DOMString autocomplete;
attribute DOMString value;
// boolean - значит, что autofocus хранит логический тип данных (true/false)
attribute boolean autofocus;
...
// "void" перед методом означает, что данный метод не возвращает значение
void select();
...
}
Свойство «nodeType»
Свойство nodeType предоставляет ещё один, «старомодный» способ узнать «тип» DOM-узла.
Его значением является цифра:
elem.nodeType == 1для узлов-элементов,elem.nodeType == 3для текстовых узлов,elem.nodeType == 9для объектов документа,- В спецификации можно посмотреть остальные значения.
Например:
<body>
<script>
let elem = document.body;
// давайте разберёмся: какой тип узла находится в elem?
alert(elem.nodeType); // 1 => элемент
// и его первый потомок...
alert(elem.firstChild.nodeType); // 3 => текст
// для объекта document значение типа -- 9
alert( document.nodeType ); // 9
</script>
</body>
В современных скриптах, чтобы узнать тип узла, мы можем использовать метод instanceof и другие способы проверить класс, но иногда nodeType проще использовать. Мы не можем изменить значение nodeType, только прочитать его.
Тег: nodeName и tagName
Получив DOM-узел, мы можем узнать имя его тега из свойств nodeName и tagName:
Например:
Есть ли какая-то разница между tagName и nodeName?
Да, она отражена в названиях свойств, но не очевидна.
- Свойство
tagNameесть только у элементовElement. - Свойство
nodeNameопределено для любых узловNode:- для элементов оно равно
tagName. - для остальных типов узлов (текст, комментарий и т.д.) оно содержит строку с типом узла.
- для элементов оно равно
Другими словами, свойство tagName есть только у узлов-элементов (поскольку они происходят от класса Element), а nodeName может что-то сказать о других типах узлов.
Например, сравним tagName и nodeName на примере объекта document и узла-комментария:
<body><!-- комментарий -->
<script>
// для комментария
alert( document.body.firstChild.tagName ); // undefined (не элемент)
alert( document.body.firstChild.nodeName ); // #comment
// for document
alert( document.tagName ); // undefined (не элемент)
alert( document.nodeName ); // #document
</script>
</body>
Если мы имеем дело только с элементами, то можно использовать tagName или nodeName, нет разницы.
В браузере существуют два режима обработки документа: HTML и XML. HTML-режим обычно используется для веб-страниц. XML-режим включается, если браузер получает XML-документ с заголовком: Content-Type: application/xml+xhtml.
В HTML-режиме значения tagName/nodeName всегда записаны в верхнем регистре. Будет выведено BODY вне зависимости от того, как записан тег в HTML <body> или <BoDy>.
В XML-режиме регистр сохраняется «как есть». В настоящее время XML-режим применяется редко.
innerHTML: содержимое элемента
Свойство innerHTML позволяет получить HTML-содержимое элемента в виде строки.
Мы также можем изменять его. Это один из самых мощных способов менять содержимое на странице.
Пример ниже показывает содержимое document.body, а затем полностью заменяет его:
Мы можем попробовать вставить некорректный HTML, браузер исправит наши ошибки:
Если innerHTML вставляет в документ тег <script> – он становится частью HTML, но не запускается.
Будьте внимательны: «innerHTML+=» осуществляет перезапись
Мы можем добавить HTML к элементу, используя elem.innerHTML+="ещё html".
Вот так:
chatDiv.innerHTML += "<div>Привет<img src='smile.gif'/> !</div>";
chatDiv.innerHTML += "Как дела?";
На практике этим следует пользоваться с большой осторожностью, так как фактически происходит не добавление, а перезапись.
Технически эти две строки делают одно и то же:
elem.innerHTML += "...";
// это более короткая запись для:
elem.innerHTML = elem.innerHTML + "..."
Другими словами, innerHTML+= делает следующее:
- Старое содержимое удаляется.
- На его место становится новое значение
innerHTML(с добавленной строкой).
Так как содержимое «обнуляется» и переписывается заново, все изображения и другие ресурсы будут перезагружены.
В примере chatDiv выше строка chatDiv.innerHTML+="Как дела?" заново создаёт содержимое HTML и перезагружает smile.gif (надеемся, картинка закеширована). Если в chatDiv много текста и изображений, то эта перезагрузка будет очень заметна.
Есть и другие побочные эффекты. Например, если существующий текст выделен мышкой, то при переписывании innerHTML большинство браузеров снимут выделение. А если это поле ввода <input> с текстом, введённым пользователем, то текст будет удалён. И т.д.
К счастью, есть и другие способы добавить содержимое, не использующие innerHTML, которые мы изучим позже.
outerHTML: HTML элемента целиком
Свойство outerHTML содержит HTML элемента целиком. Это как innerHTML плюс сам элемент.
Посмотрим на пример:
Будьте осторожны: в отличие от innerHTML, запись в outerHTML не изменяет элемент. Вместо этого элемент заменяется целиком во внешнем контексте.
Да, звучит странно, и это действительно необычно, поэтому здесь мы и отмечаем это особо.
Рассмотрим пример:
Какая-то магия, да?
В строке (*) мы заменили div на <p>Новый элемент</p>. Во внешнем документе мы видим новое содержимое вместо <div>. Но, как видно в строке (**), старая переменная div осталась прежней!
Это потому, что использование outerHTML не изменяет DOM-элемент, а удаляет его из внешнего контекста и вставляет вместо него новый HTML-код.
То есть, при div.outerHTML=... произошло следующее:
divбыл удалён из документа.- Вместо него был вставлен другой HTML
<p>Новый элемент</p>. - В
divосталось старое значение. Новый HTML не сохранён ни в какой переменной.
Здесь легко сделать ошибку: заменить div.outerHTML, а потом продолжить работать с div, как будто там новое содержимое. Но это не так. Подобное верно для innerHTML, но не для outerHTML.
Мы можем писать в elem.outerHTML, но надо иметь в виду, что это не меняет элемент, в который мы пишем. Вместо этого создаётся новый HTML на его месте. Мы можем получить ссылки на новые элементы, обратившись к DOM.
nodeValue/data: содержимое текстового узла
Свойство innerHTML есть только у узлов-элементов.
У других типов узлов, в частности, у текстовых, есть свои аналоги: свойства nodeValue и data. Эти свойства очень похожи при использовании, есть лишь небольшие различия в спецификации. Мы будем использовать data, потому что оно короче.
Прочитаем содержимое текстового узла и комментария:
Мы можем представить, для чего нам может понадобиться читать или изменять текстовый узел, но комментарии?
Иногда их используют для вставки информации и инструкций шаблонизатора в HTML, как в примере ниже:
<!-- if isAdmin -->
<div>Добро пожаловать, Admin!</div>
<!-- /if -->
...Затем JavaScript может прочитать это из свойства data и обработать инструкции.
textContent: просто текст
Свойство textContent предоставляет доступ к тексту внутри элемента за вычетом всех <тегов>.
Например:
Как мы видим, возвращается только текст, как если бы все <теги> были вырезаны, но текст в них остался.
На практике редко появляется необходимость читать текст таким образом.
Намного полезнее возможность записывать текст в textContent, т.к. позволяет писать текст «безопасным способом».
Представим, что у нас есть произвольная строка, введённая пользователем, и мы хотим показать её.
- С
innerHTMLвставка происходит «как HTML», со всеми HTML-тегами. - С
textContentвставка получается «как текст», все символы трактуются буквально.
Сравним два тега div:
- В первый
<div>имя приходит «как HTML»: все теги стали именно тегами, поэтому мы видим имя, выделенное жирным шрифтом. - Во второй
<div>имя приходит «как текст», поэтому мы видим<b>Винни-пух!</b>.
В большинстве случаев мы рассчитываем получить от пользователя текст и хотим, чтобы он интерпретировался как текст. Мы не хотим, чтобы на сайте появлялся произвольный HTML-код. Присваивание через textContent – один из способов от этого защититься.
Свойство «hidden»
Атрибут и DOM-свойство «hidden» указывает на то, видим ли мы элемент или нет.
Мы можем использовать его в HTML или назначать при помощи JavaScript, как в примере ниже:
Технически, hidden работает так же, как style="display:none". Но его применение проще.
Мигающий элемент:
Другие свойства
У DOM-элементов есть дополнительные свойства, в частности, зависящие от класса:
value– значение для<input>,<select>и<textarea>(HTMLInputElement,HTMLSelectElement...).href– адрес ссылки «href» для<a href="...">(HTMLAnchorElement).id– значение атрибута «id» для всех элементов (HTMLElement).- ...и многие другие...
Например:
Большинство стандартных HTML-атрибутов имеют соответствующее DOM-свойство, и мы можем получить к нему доступ.
Если мы хотим узнать полный список поддерживаемых свойств для данного класса, можно найти их в спецификации. Например, класс HTMLInputElement описывается здесь: https://html.spec.whatwg.org/#htmlinputelement.
Если же нам нужно быстро что-либо узнать или нас интересует специфика определённого браузера – мы всегда можем вывести элемент в консоль, используя console.dir(elem), и прочитать все свойства. Или исследовать «свойства DOM» во вкладке Elements браузерных инструментов разработчика.
Итого
Каждый DOM-узел принадлежит определённому классу. Классы формируют иерархию. Весь набор свойств и методов является результатом наследования.
Главные свойства DOM-узла:
nodeType- Свойство
nodeTypeпозволяет узнать тип DOM-узла. Его значение – числовое:1для элементов,3для текстовых узлов, и т.д. Только для чтения. nodeName/tagName- Для элементов это свойство возвращает название тега (записывается в верхнем регистре, за исключением XML-режима). Для узлов-неэлементов
nodeNameописывает, что это за узел. Только для чтения. innerHTML- Внутреннее HTML-содержимое узла-элемента. Можно изменять.
outerHTML- Полный HTML узла-элемента. Запись в
elem.outerHTMLне меняетelem. Вместо этого она заменяет его во внешнем контексте. nodeValue/data- Содержимое узла-неэлемента (текст, комментарий). Эти свойства практически одинаковые, обычно мы используем
data. Можно изменять. textContent- Текст внутри элемента: HTML за вычетом всех
<тегов>. Запись в него помещает текст в элемент, при этом все специальные символы и теги интерпретируются как текст. Можно использовать для защиты от вставки произвольного HTML кода. hidden- Когда значение установлено в
true, делает то же самое, что и CSSdisplay:none.
В зависимости от своего класса DOM-узлы имеют и другие свойства. Например у элементов <input> (HTMLInputElement) есть свойства value, type, у элементов <a> (HTMLAnchorElement) есть href и т.д. Большинство стандартных HTML-атрибутов имеют соответствующие свойства DOM.
Впрочем, HTML-атрибуты и свойства DOM не всегда одинаковы, мы увидим это в следующей главе.
Комментарии
<code>, для нескольких строк кода — тег<pre>, если больше 10 строк — ссылку на песочницу (plnkr, JSBin, codepen...)