DOM (Document Object Model) это дерево объектов, которое браузер строит из HTML. JavaScript не трогает HTML-текст: он работает с узлами этого дерева, а браузер перерисовывает страницу по изменениям.
Узлы и элементы
Узел (Node) бывает разный: элемент, текст, комментарий, сам document. Элемент (Element) это только теги. Отсюда пары свойств:
childNodes- все дочерние узлы, включая текст и переносы строк;children- только дочерние элементы.
<ul id="list">
<li>Один</li>
</ul>
const ul = document.getElementById('list');
ul.childNodes.length; // 3: текст "\n ", li, текст "\n"
ul.children.length; // 1
То же с firstChild / firstElementChild, nextSibling / nextElementSibling, parentNode / parentElement.
Поиск
getElementById(id)- один элемент илиnull.querySelector(css)- первый подходящий илиnull.querySelectorAll(css)- статичныйNodeList: снимок на момент вызова.getElementsByClassName,getElementsByTagName- живаяHTMLCollection: сама обновляется при изменении DOM.el.closest(css)- ближайший предок (включая сам элемент), подходящий под селектор. Главный помощник при делегировании событий.el.matches(css)- проверка, подходит ли элемент под селектор.
Живая коллекция в цикле с удалением даёт сюрпризы: элементы сдвигаются, и каждый второй пропускается. Снимок из querySelectorAll или Array.from(...) от этого защищает.
Создание и вставка
const li = document.createElement('li');
li.textContent = 'Новый';
ul.append(li); // в конец, принимает узлы и строки
ul.prepend('В начало'); // строка вставится как текст
li.before(other); // перед li
li.replaceWith(x); // заменить
li.remove(); // удалить
Если вставить узел, который уже есть в документе, он переместится, а не скопируется. Для копии есть node.cloneNode(true) (true - глубоко, с потомками; обработчики из addEventListener не копируются).
Для пачки вставок используют DocumentFragment: собрали в нём узлы, вставили один раз. Современные браузеры и так группируют изменения до следующего кадра, но фрагмент удобен и избавляет от промежуточных чтений геометрии.
textContent, innerText, innerHTML
textContent- весь текст узла, включая скрытый, без учёта CSS. Быстрый, безопасный: присваивание строки никогда не создаёт разметку.innerText- текст «как видит пользователь»: учитывает CSS, скрытое не возвращает, при чтении вызывает пересчёт layout.innerHTML- разметка. Присваивание парсит HTML: пользовательские данные туда класть нельзя, это XSS. Кроме того, перезаписьinnerHTMLпересоздаёт всех потомков, и старые обработчики и состояние полей пропадают.
Для вставки разметки рядом есть insertAdjacentHTML('beforeend', html): не трогает существующих потомков, но так же опасна с чужими данными.
Атрибуты и свойства
Атрибут живёт в HTML, свойство в JS-объекте. Часто они синхронизированы, но не всегда:
input.getAttribute('value'); // начальное значение из разметки
input.value; // текущее, что ввёл пользователь
value у поля ввода: атрибут хранит значение по умолчанию, свойство текущее. href как свойство возвращает абсолютный URL, а атрибут как написано. Для data-* атрибутов есть el.dataset: data-user-id превращается в dataset.userId.
Классы: el.classList.add/remove/toggle/contains. Стили: el.style пишет только инлайн-стили, а итоговые вычисленные значения читает getComputedStyle(el).
Частые ошибки
- Ждать, что
querySelectorAllвернёт массив: этоNodeList, у него естьforEach, но нетmapиfilter. НуженArray.fromили спред. - Путать
childrenиchildNodes. - Вставлять пользовательский ввод через
innerHTML. - Запускать скрипт в
<head>безdeferи получатьnullотgetElementById: элемента ещё нет в DOM.
Что спросят на собесе
- Чем живая коллекция отличается от статичной, кто какую возвращает.
- Разница
textContent,innerTextиinnerHTML, почемуinnerHTMLопасен. - Атрибут и свойство: пример, где они расходятся.
- Как вставить много элементов эффективно.
- Что делают
closestиmatches.