DOM-дерево: узлы и типы узлов

DOM-дерево — иерархическая структура объектов (узлов), представляющих HTML-документ в памяти браузера; каждый узел имеет тип (Element, Text, Comment и др.) и набор свойств и методов.

Зачем нужно

Понимание типов узлов DOM объясняет поведение childNodes vs children, nodeValue vs textContent, почему пробелы между тегами создают Text-узлы. Без этого знания легко получить неожиданные результаты при обходе дерева или работе с childNodes. Знание интерфейсов Node, Element, HTMLElement помогает правильно применять методы API.

Где используется

  • Обход DOM-дерева: рекурсивный поиск, фильтрация узлов по типу
  • Работа с childNodes: нужно фильтровать текстовые узлы/комментарии
  • Клонирование и перемещение узлов: cloneNode(true/false)
  • Работа с шаблонами: <template> содержит DocumentFragment
  • Серверный рендеринг: понимание структуры для гидрации

Основной контент

Иерархия интерфейсов

EventTarget
  └── Node
        ├── Document (nodeType: 9)
        ├── DocumentFragment (nodeType: 11)
        ├── Element (nodeType: 1)
        │     └── HTMLElement
        │           ├── HTMLDivElement
        │           ├── HTMLInputElement
        │           └── ...
        ├── Text (nodeType: 3)
        ├── Comment (nodeType: 8)
        └── ProcessingInstruction (nodeType: 7)

Типы узлов и nodeType

// Константы Node
console.log(Node.ELEMENT_NODE);                // 1
console.log(Node.TEXT_NODE);                   // 3
console.log(Node.COMMENT_NODE);               // 8
console.log(Node.DOCUMENT_NODE);              // 9
console.log(Node.DOCUMENT_FRAGMENT_NODE);     // 11

// Определение типа узла
function describeNode(node) {
  switch (node.nodeType) {
    case Node.ELEMENT_NODE:
      return `Элемент: <${node.tagName.toLowerCase()}>`;
    case Node.TEXT_NODE:
      return `Текст: "${node.nodeValue.trim()}"`;
    case Node.COMMENT_NODE:
      return `Комментарий: ${node.nodeValue}`;
    default:
      return `Узел типа ${node.nodeType}`;
  }
}

childNodes vs children

<ul id="list">
  <li>Первый</li>
  <!-- Комментарий -->
  <li>Второй</li>
</ul>
const list = document.getElementById('list');

// childNodes — ВСЕ дочерние узлы (включая Text и Comment)
console.log(list.childNodes.length); // 7: \n, li, \n, comment, \n, li, \n

// children — только дочерние Element-узлы
console.log(list.children.length);   // 2: два <li>

// Фильтрация по типу
const elementChildren = [...list.childNodes]
  .filter(n => n.nodeType === Node.ELEMENT_NODE);
// или проще:
const elements = [...list.children];

Ключевые свойства узлов

const div = document.querySelector('div');
const textNode = div.firstChild; // Text-узел

// Свойства Node (у всех узлов)
div.nodeType;       // 1
div.nodeName;       // 'DIV'
div.nodeValue;      // null (у элементов)
textNode.nodeValue; // текст (у Text-узлов)

// textContent — текст всего поддерева (работает у всех Node)
div.textContent = 'Новый текст'; // заменяет всё содержимое текстом

// Свойства Element
div.tagName;       // 'DIV'
div.innerHTML;     // HTML-содержимое как строка
div.outerHTML;     // HTML-представление самого элемента

// Навигация по дереву
div.parentNode;          // родительский Node (может быть Document)
div.parentElement;       // родительский Element (null у <html>)
div.firstChild;          // первый Node (может быть Text)
div.firstElementChild;   // первый Element
div.lastElementChild;    // последний Element
div.nextSibling;         // следующий Node
div.nextElementSibling;  // следующий Element

Создание и клонирование узлов

// Создание элементов
const el = document.createElement('div');
el.className = 'card';
el.dataset.id = '42';

// Создание текстового узла
const text = document.createTextNode('Привет');
el.appendChild(text);

// Создание комментария
const comment = document.createComment('TODO: убрать позже');
document.body.appendChild(comment);

// Клонирование
const original = document.getElementById('template-item');
const clone = original.cloneNode(true);  // true — глубокое (с дочерними)
const shallow = original.cloneNode(false); // false — только сам узел

DocumentFragment

// DocumentFragment — «виртуальный» контейнер, не часть DOM
const fragment = document.createDocumentFragment;

for (let i = 0; i < 100; i++) {
  const li = document.createElement('li');
  li.textContent = `Пункт ${i}`;
  fragment.appendChild(li); // нет reflow
}

document.getElementById('list').appendChild(fragment); // один reflow
// После вставки fragment пуст — дочерние перешли в DOM

Частые ошибки

  • childNodes вместо children: пробелы между тегами создают Text-узлы — childNodes[0] часто оказывается текстовым узлом \n, а не первым элементом.
  • Модификация childNodes во время итерации: childNodes — живая коллекция. Итерация по ней с одновременным удалением элементов пропускает узлы — преобразуйте в массив: [...el.childNodes].
  • nodeValue у Element-узлов: у элементов nodeValue === null. Для получения текста элемента используйте textContent.

Связанные темы

Ресурсы