Давайте тепер більш уважно подивимося на вузли DOM.
У цьому розділі ми більше розглянемо те, чим вони являються та вивчаємо їх найбільш використані властивості.
Класи DOM вузлів
Різні вузли DOM можуть мати різні властивості. Наприклад, вузол-елемент, що відповідає тегу <a>
, має властивості, які пов’язані з посиланням, а той вузол-елемент, що відповідає <input>
має властивості, пов’язані з полем введення, тощо. Текстові вузли не такі, як вузли елементів. Але існують також загальні властивості та методи між усіма з них, оскільки всі класи вузлів DOM утворюють єдину ієрархію.
Кожен вузол DOM належить до відповідного вбудованого класу.
Коренем ієрархії є EventTarget, від нього успадковується Node, а інші вузли DOM успадкують вже від нього.
Ось рисунок, на якому слідує пояснення:
Класи:
- EventTarget – це кореневий “абстрактний” клас. Об’єкти цього класу ніколи не створюються. Він служить основою, тому всі вузли DOM підтримують так звані “події”, які ми розглянемо пізніше.
- Node – це також “абстрактний” клас, що служить базою для вузлів DOM. Він забезпечує основну функціональність дерева:
parentNode
,nextSibling
,childNodes
і так далі (це гетери). Об’єкти класуNode
ніколи не створюються. Але є конкретні класи вузлів, які успадковуються від нього, а саме:Text
для текстових вузлів,Element
для вузлів-елементів та більш екзотичні, такі якComment
для вузлів-коментарів. - Element – це базовий клас для елементів DOM. Він забезпечує навігацію на рівні елементів, таку як
nextElementSibling
,children
та пошукові методи, такі якgetElementsByTagName
,querySelector
. Браузер підтримує не тільки HTML, але й XML та SVG. КласElement
служить базою для більш конкретних класів:SVGElement
,XMLElement
таHTMLElement
… - HTMLElement – це, нарешті, основний клас для всіх елементів HTML. Він успадковується конкретними елементами 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
…Або ми можемо просто викликати toString
:
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
.
У специфікації, класи DOM описані не за допомогою JavaScript, а спеціальною мовою опису інтерфейсу(IDL), яку зазвичай легко зрозуміти.
У IDL всі властивості призводять до їх типів. Наприклад, DOMString
, boolean
тощо.
Ось витяг з цієї специфікації, з коментарями:
// Define HTMLInputElement
// Двакрапка ":" означає, що HTMLInputElement наслідується від HTMLElement
interface HTMLInputElement: HTMLElement {
// тут визначаються всі властивості та методи елементів <input>
// "DOMString" означає, що значенням властивості є рядок
attribute DOMString accept;
attribute DOMString alt;
attribute DOMString autocomplete;
attribute DOMString value;
// булева властивість (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;
// перевіримо, що це таке?
alert(elem.nodeType); // 1 => елемент
// і перший дочірній елемент ...
alert(elem.firstChild.nodeType); // 3 => текст
// для об’єкта документа тип -- 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
and nodeName
для вузла-документа та коментаря:
<body><!-- comment -->
<script>
// для коментаря
alert( document.body.firstChild.tagName ); // undefined (не елемент)
alert( document.body.firstChild.nodeName ); // #comment
// для документа
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
як для <body>
, так і для <BoDy>
.
У режимі XML регістр літер зберігається “як є”. В даний час XML режим рідко використовується.
innerHTML: вміст
Властивість innerHTML дозволяє отримати HTML всередині елемента як рядок.
Ми також можемо це змінити. Отже, це один з найпотужніших способів зміни сторінку.
На прикладі показано вміст document.body
який потім повністю замінюється.
<body>
<p>A paragraph</p>
<div>A div</div>
<script>
alert( document.body.innerHTML ); // читаємо поточний вміст
document.body.innerHTML = 'Новий BODY!'; // замінюємо його
</script>
</body>
Ми можемо спробувати вставити невалідний HTML, браузер виправить наші помилки:
<body>
<script>
document.body.innerHTML = '<b>test'; // забули закрити тег
alert( document.body.innerHTML ); // <b>test</b> (виправлено)
</script>
</body>
Якщо innerHTML
вставляє тег <script>
у документ – він стає частиною HTML, але не виконується.
Остерігайтеся: “innerHTML+=” робить повний перезапис
Ми можемо додати HTML до елемента за допомогою elem.innerHTML+="more html"
.
Як наприклад:
chatDiv.innerHTML += "<div>Привіт<img src='smile.gif'/> !</div>";
chatDiv.innerHTML += "Як справи?";
Але ми повинні бути дуже обережними щодо цього, тому що те, що відбувається – це не додавання, але повний перезапис.
Технічно ці два рядки роблять те ж саме:
elem.innerHTML += "...";
// коротший спосіб записати:
elem.innerHTML = elem.innerHTML + "..."
Іншими словами, innerHTML+=
робить наступне:
- Старий вміст видаляється.
- Замість нього написано новий
innerHTML
(конкатенація старого та нового).
Оскільки вміст є “нульовим” та переписаним з нуля, всі зображення та інші ресурси будуть перезавантажені.
У прикладі chatDiv
вище рядок chatDiv.innerHTML+="How goes?"
заново створює вміст HTML та перезавантажує зображення smile.gif
(сподіваюся, що воно закешоване). Якщо chatDiv
має багато іншого тексту та зображень, то перезавантаження стає чітко видно.
Також є й інші побічні ефекти. Наприклад, якщо існуючий текст був виділений за допомогою миші, то більшість браузерів видалять виділений текст при перезаписування innerHTML
. І якщо був <input>
з текстом, який вводиться відвідувачем, то текст буде видалено. І так далі.
На щастя, є й інші способи додати HTML, крім 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
не змінює елемент. Замість цього він замінює його в DOM.
Так, це звучить дивно, так воно і є, ось тому ми робимо окрему примітку про це тут. Поглянь.
Розглянемо приклад:
<div>Привіт, світ!</div>
<script>
let div = document.querySelector('div');
// replace div.outerHTML with <p>...</p>
div.outerHTML = '<p>Новий елемент</p>'; // (*)
// Ого! 'div' все ще те ж саме!
alert(div.outerHTML); // <div>Привіт, світ!</div> (**)
</script>
Виглядає дуже дивно, вірно?
У рядку (*)
ми замінили div
на <p>Новий елемент</p>
. У зовнішньому документі (DOM) ми можемо побачити новий вміст замість <div>
. Але, як ми можемо бачити в рядку (**)
, значення старого div
не змінилося!
Присвоєння outerHTML
не змінює елемент DOM (об’єкт, на який посилається, у цьому випадку змінний ‘div’), але його видаляє з DOM та вставляє новий HTML у своєму місці.
Отже, в div.outerHTML=...
сталося наступне:
div
був видалений з документа.- Інший шматок HTML
<p>Новий елемент</p>
був вставлений на його місце. div
ще має своє старе значення. Новий HTML не був збережений для будь-якої змінної.
Тут так легко зробити помилку: змініть div.outerHTML
, а потім продовжуйте працювати з div
так, наче він має новий вміст у собі. Але це не так. Така річ є правильною для innerHTML
, але не для outerHTML
.
Ми можемо записати в elem.outerHTML
, але слід пам’ятати, що це не змінює елемент, в який ми пишемо (‘elem’). Це вставить замість цього новий 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>Ласкаво просимо, Адмін!</div>
<!-- /if -->
…Тоді JavaScript може прочитати його з data
властивості та обробити вбудовані інструкції.
textContent: чистий текст
textContent
надає доступ до тексту всередині елемента: тільки текст, мінус всі <теги>
.
Наприклад:
<div id="news">
<h1>Заголовок!</h1>
<p>Марсіанці нападають на людей!</p>
</div>
<script>
// Заголовок! Марсіанці нападають на людей!
alert(news.textContent);
</script>
Як ми бачимо, повертається лише текст, як ніби всі <tags>
були вирізані, але текст у них залишився.
На практиці читання такого тексту рідко потрібне.
Запис в textContent
набагато корисніше, тому що це дозволяє записати текст “безпечним способом”.
Скажімо, у нас є довільний рядок, наприклад той, що ввів користувач, і який він хочете показати.
- З
innerHTML
ми його вставили “як HTML”, з усіма HTML-тегами. - З
textContent
ми вставимо це “як текст”, всі символи обробляються буквально.
Порівняйте ці два підходи:
<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”
Атрибут “hidden” та властивість DOM визначає, чи видно елемент чи ні.
Ми можемо використовувати її в 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="value">
<script>
alert(elem.type); // "text"
alert(elem.id); // "elem"
alert(elem.value); // значення
</script>
Найбільш стандартні атрибути HTML мають відповідну DOM властивість, і ми можемо отримати доступ до нєї.
Якщо ми хочемо знати повний список підтримуваних властивостей для заданого класу, ми можемо знайти їх у специфікації. Наприклад, HTMLInputElement
задокументовано на https://html.spec.whatwg.org/#htmlinpelement.
Або якщо ми хотіли б отримати їх швидко, або зацікавлені в конкретному специфікації браузера – ми завжди можемо вивести елемент за допомогою console.dir(elem)
та прочитати властивості. Або вивчити “DOM properties” на вкладці “Elements” інструментів розробника браузера.
Підсумки
Кожен вузол DOM належить до певного класу. Класи утворюють ієрархію. Повний набір властивостей та методів приходить як результат наслідування.
Основні властивості DOM вузла це:
nodeType
- Ми можемо використовувати цью властивість, щоб побачити, чи є вузол текстовим чи елементом. Вона має числове значення:
1
для елементів,3
для текстових вузлів, а також кілька інших значень для інших типів вузлів. Лише для читання. nodeName/tagName
- Для елементів – це назва тегів (записуються в верхньому регістрі, якщо не XML-режим). Для неелементних вузлів
nodeName
описує, що це таке. Лише для читання. innerHTML
- Вміст HTML елемента. Можна змінювати.
outerHTML
- Повний HTML елемента. Операція запису в
elem.outerHTML
не змінює самelem
. Замість цього він замінюється новим HTML у зовнішньому контексті. nodeValue/data
- Вміст неелементного вузла (тексту, коментаря). Ці дві властивості майже однакові, зазвичай ми використовуємо
data
. Можна змінювати. textContent
- Текст всередині елемента: HTML мінус всі
<теги>
. Написане в ній вставиться текстом всередину елемента, з усіма спеціальними символами та тегами, які обробляються точно так, як текст. Дозволяє безпечно вставити користувацький текст і захистити від небажаних HTML-вставок. hidden
- Коли встановлено
true
, робитьте ж саме, що й CSSdisplay:none
.
DOM вузли також мають інші властивості залежно від їх класу. Наприклад, <input>
елементи (HTMLInputElement
) підтримують value
, type
, тоді як елементи <a>
(HTMLAnchorElement
) підтримують href
та ін. Більшість стандартних атрибутів HTML мають відповідні властивості.
Однак атрибути HTML та властивості DOM не завжди однакові, як ми побачимо у наступному розділі.
Коментарі
<code>
, для кількох рядків – обгорніть їх тегом<pre>
, для понад 10 рядків – використовуйте пісочницю (plnkr, jsbin, codepen…)