17 лютого 2022 р.

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

Давайте тепер більш уважно подивимося на вузли 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:

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

  // перевіримо, що це таке?
  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 – немає ніякої різниці.

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

  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’). Це вставить замість цього новий 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>
  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…)