Теперь давайте более внимательно взглянем на 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 содержится его имя:
alert( document.body.constructor.name ); // HTMLBodyElement
…Или мы можем просто привести его к строке:
alert( document.body ); // [object HTMLBodyElement]
Проверить наследование можно также при помощи instanceof:
alert( document.body instanceof HTMLBodyElement ); // true
alert( document.body instanceof HTMLElement ); // true
alert( document.body instanceof Element ); // true
alert( document.body instanceof Node ); // true
alert( document.body instanceof EventTarget ); // true
Как видно, 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:
Например:
alert( document.body.nodeName ); // BODY
alert( document.body.tagName ); // BODY
Есть ли какая-то разница между 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, а затем полностью заменяет его:
<body>
<p>Параграф</p>
<div>DIV</div>
<script>
alert( document.body.innerHTML ); // читаем текущее содержимое
document.body.innerHTML = 'Новый BODY!'; // заменяем содержимое
</script>
</body>
Мы можем попробовать вставить некорректный HTML, браузер исправит наши ошибки:
<body>
<script>
document.body.innerHTML = '<b>тест'; // забыли закрыть тег
alert( document.body.innerHTML ); // <b>тест</b> (исправлено)
</script>
</body>
Если 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 плюс сам элемент.
Посмотрим на пример:
<div id="elem">Привет <b>Мир</b></div>
<script>
alert(elem.outerHTML); // <div id="elem">Привет <b>Мир</b></div>
</script>
Будьте осторожны: в отличие от innerHTML, запись в outerHTML не изменяет элемент. Вместо этого элемент заменяется целиком во внешнем контексте.
Да, звучит странно, и это действительно необычно, поэтому здесь мы и отмечаем это особо.
Рассмотрим пример:
<div>Привет, мир!</div>
<script>
let div = document.querySelector('div');
// заменяем div.outerHTML на <p>...</p>
div.outerHTML = '<p>Новый элемент</p>'; // (*)
// Содержимое div осталось тем же!
alert(div.outerHTML); // <div>Привет, мир!</div> (**)
</script>
Какая-то магия, да?
В строке (*) мы заменили 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, потому что оно короче.
Прочитаем содержимое текстового узла и комментария:
<body>
Привет
<!-- Комментарий -->
<script>
let text = document.body.firstChild;
alert(text.data); // Привет
let comment = text.nextSibling;
alert(comment.data); // Комментарий
</script>
</body>
Мы можем представить, для чего нам может понадобиться читать или изменять текстовый узел, но комментарии?
Иногда их используют для вставки информации и инструкций шаблонизатора в HTML, как в примере ниже:
<!-- if isAdmin -->
<div>Добро пожаловать, Admin!</div>
<!-- /if -->
…Затем JavaScript может прочитать это из свойства data и обработать инструкции.
textContent: просто текст
Свойство textContent предоставляет доступ к тексту внутри элемента за вычетом всех <тегов>.
Например:
<div id="news">
<h1>Срочно в номер!</h1>
<p>Марсиане атаковали человечество!</p>
</div>
<script>
// Срочно в номер! Марсиане атаковали человечество!
alert(news.textContent);
</script>
Как мы видим, возвращается только текст, как если бы все <теги> были вырезаны, но текст в них остался.
На практике редко появляется необходимость читать текст таким образом.
Намного полезнее возможность записывать текст в textContent, т.к. позволяет писать текст «безопасным способом».
Представим, что у нас есть произвольная строка, введённая пользователем, и мы хотим показать её.
- С
innerHTMLвставка происходит «как HTML», со всеми HTML-тегами. - С
textContentвставка получается «как текст», все символы трактуются буквально.
Сравним два тега div:
<div id="elem1"></div>
<div id="elem2"></div>
<script>
let name = prompt("Введите ваше имя?", "<b>Винни-пух!</b>");
elem1.innerHTML = name;
elem2.textContent = name;
</script>
- В первый
<div>имя приходит «как HTML»: все теги стали именно тегами, поэтому мы видим имя, выделенное жирным шрифтом. - Во второй
<div>имя приходит «как текст», поэтому мы видим<b>Винни-пух!</b>.
В большинстве случаев мы рассчитываем получить от пользователя текст и хотим, чтобы он интерпретировался как текст. Мы не хотим, чтобы на сайте появлялся произвольный HTML-код. Присваивание через textContent – один из способов от этого защититься.
Свойство «hidden»
Атрибут и DOM-свойство «hidden» указывает на то, видим ли мы элемент или нет.
Мы можем использовать его в HTML или назначать при помощи JavaScript, как в примере ниже:
<div>Оба тега DIV внизу невидимы</div>
<div hidden>С атрибутом "hidden"</div>
<div id="elem">С назначенным JavaScript свойством "hidden"</div>
<script>
elem.hidden = true;
</script>
Технически, hidden работает так же, как style="display:none". Но его применение проще.
Мигающий элемент:
<div id="elem">Мигающий элемент</div>
<script>
setInterval(() => elem.hidden = !elem.hidden, 1000);
</script>
Другие свойства
У DOM-элементов есть дополнительные свойства, в частности, зависящие от класса:
value– значение для<input>,<select>и<textarea>(HTMLInputElement,HTMLSelectElement…).href– адрес ссылки «href» для<a href="...">(HTMLAnchorElement).id– значение атрибута «id» для всех элементов (HTMLElement).- …и многие другие…
Например:
<input type="text" id="elem" value="значение">
<script>
alert(elem.type); // "text"
alert(elem.id); // "elem"
alert(elem.value); // значение
</script>
Большинство стандартных 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…)