DOM: страница как дерево, которое можно менять

Зачем:Браузер хранит страницу как дерево, и JavaScript может в нём искать, читать и менять. После урока вы находите элементы селекторами, создаёте новые из данных и понимаете, почему чужой текст нельзя вставлять через innerHTML.

25 минут

Озвучено синтезом речи (Qwen3-TTS), голос не принадлежит человеку.

DOM — объектная модель документа: дерево узлов, которое браузер строит из HTML. JavaScript находит в нём элементы, меняет их и создаёт новые.

Страница, которую можно потрогать

В уроке про рендеринг вы видели, что браузер читает HTML и строит из него DOM (Document Object Model, объектную модель документа): дерево, в котором каждый тег, каждый кусок текста и каждый атрибут становятся узлом. Страница на экране рисуется с этого дерева. Главное для нас: браузер отдаёт это дерево JavaScript. В скрипте оно доступно под именем document, и всё, что скрипт в нём поменяет, браузер тут же перерисует.

Так делается любая интерактивность: меню открывается, потому что скрипт убрал у элемента атрибут hidden; счётчик растёт, потому что скрипт записал в элемент новое число; список появляется, потому что скрипт создал для каждого пункта элемент и вставил его на страницу. Три действия: найти, прочитать или изменить, создать и вставить.

Найти элемент

Для поиска в языке есть две команды, и в них те же селекторы, что вы писали в CSS: h3, .card, #offer, nav a.

const first = document.querySelector('.card');          // первый подходящий или null
const all = document.querySelectorAll('#offer .card');  // все подходящие

querySelector возвращает один элемент или null, если ничего не нашлось. querySelectorAll возвращает список узлов (NodeList), возможно пустой. Его можно обойти через forEach или for...of, а чтобы пользоваться map и filter, превращают в массив: Array.from(all).

Из найденного элемента можно искать дальше: card.querySelector('h3') ищет только внутри этой карточки. Поэтому селектор можно писать короче, а скрипт получается аккуратнее.

Попробуйте селекторы на маленькой странице, и найденное подсветится и в разметке, и в дереве:

Найдите элементы селектором

Ничего не найдено. querySelector вернул бы null, а querySelectorAll пустой список.

Проверьте себя: что вернёт querySelector, если элемента нет?

null. Поэтому, когда элемент может отсутствовать, результат проверяют, иначе следующая строка вроде el.textContent = ... упадёт с ошибкой.

Прочитать и изменить

Когда элемент найден, у него есть свойства и методы:

  • textContent читает и записывает текст элемента: title.textContent = 'Новое название';
  • getAttribute('href') и setAttribute('href', '#top') работают с атрибутами. У многих атрибутов есть и короткие свойства: link.href, input.value, el.id, el.hidden;
  • classList.add('selected'), classList.remove('selected'), classList.toggle('selected') управляют классами. Это лучший способ менять вид: скрипт навешивает класс, а CSS знает, что с ним делать. Так оформление остаётся в CSS, а логика в JS;
  • dataset даёт доступ к атрибутам вида data-price="1200": card.dataset.price. Через них разметка передаёт скрипту свои данные.

Создать и вставить

Чтобы добавить новое, элемент сначала создают в памяти, настраивают, а потом вставляют в дерево:

const row = document.createElement('li');   // пока его нет на странице
row.textContent = 'Новый пункт';
list.append(row);                           // теперь есть

Другие нужные команды: prepend (в начало), remove() (убрать элемент), replaceChildren(...) (заменить всё содержимое). Если вставляете много элементов подряд, собирайте их в памяти и вставляйте одним append с несколькими аргументами: браузеру не надо перерисовывать страницу после каждого.

Почему чужой текст нельзя вставлять через innerHTML

Есть и более короткий способ: свойство innerHTML. В него записывают строку с разметкой, и браузер её разбирает:

box.innerHTML = '<p>Привет, ' + name + '</p>';

Выглядит удобно, но если name пришёл от посетителя (из поля формы, из комментария, из ответа сервера, из адреса страницы), то в нём может быть не имя, а разметка с обработчиком события. Браузер не видит разницы между «вашей» разметкой и «чужой»: он разберёт всё и выполнит чужой код от имени вашего сайта. Такая уязвимость называется XSS (cross-site scripting, межсайтовое выполнение скриптов). Через неё воруют сессии и подменяют страницы.

Правило простое: чужой текст вставляют через textContent. Он записывает строку как текст, символы < и > остаются обычными буквами, и ничего не выполняется. innerHTML оставьте для разметки, которую целиком написали вы.

Проверьте на одной и той же строке два способа:

Одна строка, два способа вставки
box.textContent = строка
текст «<img src=x onerror=alert(1)>Анна»

Безопасно. Одна строка текста; на странице она появится буквами.

box.innerHTML = строка
 

Здесь без тегов. Но защищает не строка, а привычка: чужой текст идёт через textContent.

Ничего из этого не выполняется: строка разбирается в изолированном документе, который никуда не вставляется.

Когда скрипт запускается

Ошибка, на которой спотыкаются все: скрипт ищет элемент, а его ещё нет. Обычный <script> останавливает разбор страницы и выполняется сразу, поэтому всё, что стоит в документе ниже, ему пока не видно. Выходов три: положить скрипт в самый конец страницы, добавить ему атрибут defer или сделать его модулем, как в нашем редакторе (type="module"). Модуль выполняется после разбора страницы, и искать в нём можно что угодно.

Практика

Сайт эпохи 2014 года. У вас уже есть список карточек, но на длинной странице их не видно с первого экрана. Пусть скрипт построит из карточек быстрый переход: ссылки на каждое предложение вверху секции, плюс счётчик.

Скрипт ничего не придумывает, он читает то, что есть в разметке: находит карточки, берёт заголовок каждой и делает ссылку на неё. Добавите в разметку ещё одну карточку, и ссылка появится сама. А если карточек нет, посетитель увидит понятное сообщение, а не пустое место.

Обратите внимание, чего нет в задании: слова innerHTML. Заголовок карточки может содержать что угодно, поэтому всё идёт через textContent.

Что было не так раньше

Когда-то DOM в разных браузерах был разным: у Internet Explorer одни методы, у Netscape другие, и document.all, document.layers нужно было угадывать. Стандарт появился в конце 90-х, а его последователи сначала неохотно его поддерживали. Именно из-за этой путаницы в 2006 году родилась библиотека jQuery: одна функция $('.card'), которая работала одинаково в любом браузере. На ней сделаны миллионы сайтов, и многие работают до сих пор, а если вы читаете чужой код со знаком доллара, то перед вами jQuery.

Сегодня браузеры ведут себя одинаково, стандарт называется DOM Standard (WHATWG), а querySelector, classList и append делают то, ради чего раньше ставили библиотеку. Для новых сайтов jQuery не нужен: он весит десятки килобайт и решает проблемы, которых больше нет. Правильным осталось другое: искать элементы по селекторам, менять классы, а не стили, и не доверять чужому тексту.

Практика

Готовлю вашу страницу…

Тест

Вопрос 1 из 7

Что вернёт document.querySelector(".нет-такого"), если на странице нет подходящего элемента?

Один ответ

Карточки

Эти карточки попадут в повторение, когда вы завершите урок.

  • Что такое DOM?

    Ответ

    Дерево объектов, которое браузер строит из HTML. JavaScript получает его как document и может искать, читать и менять узлы. Страница на экране перерисовывается следом.

  • Чем querySelector отличается от querySelectorAll?

    Ответ

    querySelector находит первый подходящий элемент или null. querySelectorAll возвращает список всех подходящих (возможно, пустой). Селекторы те же, что в CSS.

  • Как добавить на страницу новый элемент?

    Ответ

    Создать createElement, настроить (textContent, атрибуты, classList) и вставить в дерево через append. Пока не вставлен, на странице его нет.

  • Почему чужой текст нельзя вставлять через innerHTML?

    Ответ

    innerHTML разбирает строку как разметку. Если в ней есть тег с обработчиком, чужой код выполнится на вашей странице (XSS). textContent показывает текст как текст.

  • Что возвращает querySelectorAll и что с этим делать?

    Ответ

    NodeList: список, который можно обойти через forEach или for...of. Чтобы применить map и filter, его сначала превращают в массив: Array.from(list).

Источники

Дальше

Теперь вы знаете: DOM: дерево страницы из JavaScript.