22 жовтня 2023 р.

Властивості вузлів: тип, тег та вміст

Давайте детальніше розглянемо вузли DOM.

У цій главі ми розглянемо їх сутність і дізнаємося про найбільш часто використовувані властивості.

Класи DOM вузлів

Різні вузли DOM можуть мати різні властивості. Наприклад, елемент, що відповідає тегу <a>, має властивості, пов’язані з посиланням, тоді як елемент, що відповідає <input>, має властивості, пов’язані з введенням даних, тощо. Текстові вузли відрізняються від вузлів елементів. Проте, між ними також існують загальні властивості та методи, оскільки всі класи вузлів DOM утворюють єдину ієрархію.

Кожен вузол DOM належить відповідному вбудованому класу.

Коренем ієрархії є EventTarget, від нього успадковується Node, а інші вузли DOM успадкують вже від нього.

На наведеному нижче рисунку показані основні класи:

Ці класи:

  • EventTarget – це кореневий “абстрактний” клас клас для всього.

    Об’єкти цього класу ніколи не створюються. Він служить основою, тому всі вузли DOM підтримують так звані “події”, які ми розглянемо пізніше.

  • Node – також “абстрактний” клас, який є базовим для вузлів DOM.

    Він надає основні функціональні можливості дерева: parentNode, nextSibling, childNodes та інші (це геттери). Об’єкти класу Node ніколи не створюються, але існують інші класи, які успадковують його (і таким чином успадковують функціональність Node).

  • Document, з історичних причин його часто успадковує HTMLDocument (хоча останні специфікації цього не вимагають), представляє собою документ в цілому.

    Глобальний об’єкт document належить саме до цього класу. Він служить точкою входу в DOM.

  • CharacterData – “абстрактний” клас, успадковується:

    • Text – клас, що відповідає тексту всередині елементів, наприклад, Привіт у <p>Привіт</p>.
    • Comment – клас для коментарів. Вони скриті від користувача, але кожен коментар стає частиною DOM.
  • Element – базовий клас для елементів DOM.

    Він надає навігаційні можливості на рівні елементів, такі як nextElementSibling, children та методи пошуку, такі як getElementsByTagName, querySelector.

    Браузер підтримує не лише HTML, а також XML та SVG. Тому клас Element служить базою для більш конкретних класів: SVGElement, XMLElement (вони нам тут не потрібні) та HTMLElement.

  • Нарешті, HTMLElement – це базовий клас для всіх HTML-елементів. Ми будемо працювати з ним більшість часу.

    Він успадковується класами конкретних HTML-елементів:

Існує багато інших тегів з власними класами, які можуть мати певні властивості та методи, тоді як деякі елементи, такі як <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.

Специфікація IDL

У специфікації, класи 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;

  // давайте перевіримо: який тип вузла в elem?
  alert(elem.nodeType); // 1 => element

  // і перший дочірній елемент ...
  alert(elem.firstChild.nodeType); // 3 => text

  // для об’єкта document тип дорівнює 9
  alert( document.nodeType ); // 9
  </script>
</body>

У сучасних скриптах ми можемо використовувати оператор instanceof та інші перевірки на основі класів для визначення типу вузла, що може бути більш зручним. Однак іноді використання властивості 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

    // для документу
    alert( document.tagName ); // undefined (це не елемент)
    alert( document.nodeName ); // #document
  </script>
</body>

Якщо ми маємо справу лише з елементами, то ми можемо використовувати як tagName, так і nodeName – немає ніякої різниці.

Назва тегів завжди написана великими літерами, за винятком режиму XML

Браузер має два режими обробки документів: 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>тест'; // забули закрити тег
    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+= робить наступне:

  1. Старий вміст видаляється.
  2. Замість нього написано новий 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’). Замість цього воно замінює його в DOM. Ми можемо отримати посилання на нові елементи, запитуючи 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>

Як ми бачимо, повертається лише текст, як ніби всі <теги> були вирізані, але текст залишився.

На практиці читання такого тексту рідко потрібне.

Запис в 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>
  1. Перший <div> отримує назву “як HTML”: всі теги стають тегами, тому ми бачимо назву жирним шрифтом.
  2. Другий <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, робитьте ж саме, що й CSS display:none.

DOM вузли також мають інші властивості залежно від їх класу. Наприклад, <input> елементи (HTMLInputElement) підтримують value, type, тоді як елементи <a> (HTMLAnchorElement) підтримують href та ін. Більшість стандартних атрибутів HTML мають відповідні властивості.

Однак атрибути HTML та властивості DOM не завжди однакові, як ми побачимо у наступному розділі.

Завдання

важливість: 5

Є дерево, що структуровано як вкладені ul/li.

Напишіть код, який для кожного <li> показує:

  1. Текст всередині вузла (без піддерева)
  2. Кількість вкладених <li> – всіх нащадків, включаючи глибоко вкладені.

Демонстрація в новому вікні

Відкрити пісочницю для завдання.

Давайте зробимо цикл по <li>:

for (let li of document.querySelectorAll('li')) {
  ...
}

У циклі нам потрібно отримати текст всередині кожного li.

Ми можемо прочитати текст з першого дочірнього вузла li, це текстовий вузол:

for (let li of document.querySelectorAll('li')) {
  let title = li.firstChild.data;

  // title -- це текст в <li> перед будь-якими іншими вузлами
}

Тоді ми можемо отримати кількість нащадків як li.getElementsByTagName('li').length.

Відкрити рішення в пісочниці.

важливість: 5

Що показує скрипт?

<html>

<body>
  <script>
    alert(document.body.lastChild.nodeType);
  </script>
</body>

</html>

Тут є пастка.

У момент виконання <script> останній вузол DOM є саме <script>, тому що браузер ще не обробив решту сторінки.

Отже, результат – 1 (вузол-елемент).

<html>

<body>
  <script>
    alert(document.body.lastChild.nodeType);
  </script>
</body>

</html>
важливість: 3

Що показує цей код?

<script>
  let body = document.body;

  body.innerHTML = "<!--" + body.tagName + "-->";

  alert( body.firstChild.data ); // що тут?
</script>

Відповідь: BODY.

<script>
  let body = document.body;

  body.innerHTML = "<!--" + body.tagName + "-->";

  alert( body.firstChild.data ); // BODY
</script>

Що відбувається крок за кроком:

  1. Вміст <body> замінюється коментарем. Коментар <!--BODY-->, тому що body.tagName == "BODY". Як ми пам’ятаємо, tagName завжди пишеться великими літерами в HTML.
  2. Коментар зараз є єдиним дочірнім вузлом, тому ми отримуємо його в body.firstChild.
  3. Властивість коментаря data – це його вміст (всередині <!--...-->): "BODY".
важливість: 4

До якого класу належить document?

Де його місце в ієрархії DOM?

Він успадковує від Node чи Element, або, можливо, HTMLElement?

Ми можемо побачити, якому класу він належить вивівши його, наприклад:

alert(document); // [object HTMLDocument]

Або:

alert(document.constructor.name); // HTMLDocument

Отже, document – це екземпляр класу HTMLDocument.

Яке його місце в ієрархії?

Так, ми могли б переглянути специфікацію, але було б швидше з’ясувати вручну.

Давайте пройдемо по ланцюгу прототипів через __proto__.

Як відомо, методи класу знаходяться в prototype конструктора. Наприклад, HTMLDocument.prototype має методи документів.

Також є посилання на функцію конструктора всередині prototype:

alert(HTMLDocument.prototype.constructor === HTMLDocument); // true

Щоб отримати назву класу як рядок, ми можемо використовувати constructor.name. Давайте зробимо це для всього прототипного ланцюга document аж до класу Node:

alert(HTMLDocument.prototype.constructor.name); // HTMLDocument
alert(HTMLDocument.prototype.__proto__.constructor.name); // Document
alert(HTMLDocument.prototype.__proto__.__proto__.constructor.name); // Node

Це ієрархія.

Ми також можемо розглянути об’єкт за допомогою console.dir(document) і побачити ці назви, відкриваючи __proto__. Консоль браузера під капотом бере їх з constructor.

Навчальна карта

Коментарі

прочитайте це, перш ніж коментувати…
  • Якщо у вас є пропозиції, щодо покращення підручника, будь ласка, створіть обговорення на GitHub або одразу створіть запит на злиття зі змінами.
  • Якщо ви не можете зрозуміти щось у статті, спробуйте покращити її, будь ласка.
  • Щоб вставити код, використовуйте тег <code>, для кількох рядків – обгорніть їх тегом <pre>, для понад 10 рядків – використовуйте пісочницю (plnkr, jsbin, codepen…)