JavaScript — обход DOM
Обход DOM (Document Object Model) — фундаментальный навык для веб-разработчиков. Освоив его, вы сможете динамически управлять страницами.
Обход DOM (Document Object Model) — фундаментальный навык для веб-разработчиков, работающих с JavaScript. Освоив обход DOM, вы сможете динамически управлять веб-страницами, создавая интерактивные и адаптивные интерфейсы. Это руководство предоставит вам подробные объяснения и множество примеров кода, которые помогут вам стать профессионалом в данной области.
Введение в обход DOM
DOM представляет структуру веб-страницы в виде дерева узлов. Каждый узел соответствует элементу, фрагменту текста или комментарию на странице. Обход DOM означает перемещение от одного узла к другому — вверх к родительскому, вниз к дочерним или горизонтально к соседним — чтобы читать или изменять элементы относительно начальной точки.
Зачем обходить DOM вместо того, чтобы просто делать выборку? Нередко вы начинаете с элемента, который у вас уже есть (например, кнопка, которую нажал пользователь), и вам нужно добраться до связанного элемента, точный id или класс которого вам заранее неизвестен — его контейнера, следующего элемента в списке или всех вложенных ответов под ним. Обход выражает идею «то, что находится рядом с / внутри / вокруг этого элемента».
В этом руководстве рассматриваются наиболее часто используемые свойства отношений:
| Направление | Свойство только для элементов | Свойство, включающее всё |
|---|---|---|
| Вниз (дочерние) | children, firstElementChild, lastElementChild | childNodes, firstChild, lastChild |
| Вверх (родительский) | parentElement | parentNode |
| Горизонтально (соседние) | nextElementSibling, previousElementSibling | nextSibling, previousSibling |
Левый столбец пропускает текстовые узлы и узлы комментариев, поэтому он почти всегда является предпочтительным. Правый столбец включает текстовые узлы с пробелами между тегами, что является распространённым источником ошибок.
Для поиска элементов в любом месте документа (а не относительно узла) смотрите Поиск: getElement* и querySelector и Выбор элементов DOM.
Понимание дерева DOM
Прежде чем углубляться в методы обхода, полезно представить дерево DOM. Вот простой HTML-документ для иллюстрации:
<!DOCTYPE html>
<html>
<head>
<title>DOM Traversal Example</title>
</head>
<body>
<div id="container">
<p class="text">Hello, World!</p>
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
</div>
</body>
</html>В этом документе элемент <body> содержит <div> с id равным container, который в свою очередь содержит элемент <p> и <ul> с дочерними элементами <li>. Чтобы узнать, как браузер классифицирует каждую часть (элемент, текст, комментарий), смотрите Понимание узлов DOM.
Основные методы обхода
Доступ к дочерним узлам
Представьте, что у вас есть блог с несколькими публикациями, и каждая публикация имеет комментарии. Вы хотите подсчитать комментарии для конкретной публикации.
<!DOCTYPE html>
<html>
<head>
<title>Accessing Child Nodes</title>
</head>
<body>
<div id="blog-post">
<h2>Blog Post Title</h2>
<p>Some interesting content...</p>
<div class="comments">
<p>Comment 1</p>
<p>Comment 2</p>
<p>Comment 3</p>
</div>
</div>
<script>
const commentsContainer = document.querySelector('.comments');
const comments = commentsContainer.children; // Only includes element nodes
// Display the number of comments
console.log(`Number of comments: ${comments.length}`);
</script>
</body>
</html>Этот код выбирает <div> с классом "comments" и отображает количество элементов-комментариев внутри него. Обратите внимание: children возвращает только узлы-элементы, тогда как childNodes включает текстовые узлы и узлы комментариев. Для обхода только элементов предпочтительнее использовать children.
Переход к родительским узлам
Представьте, что у вас есть список товаров в корзине покупок, и вы хотите найти элемент-контейнер конкретного товара.
<!DOCTYPE html>
<html>
<head>
<title>Navigating to Parent Nodes</title>
</head>
<body>
<div id="shopping-cart">
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
</div>
<script>
const cartItem = document.querySelector('li');
const parent = cartItem.parentNode;
// Display the parent node
console.log(`The parent of the first cart item is a: ${parent.tagName}`);
</script>
</body>
</html>Этот код выбирает первый элемент <li> и отображает имя тега его родительского узла. Для навигации только по элементам parentElement часто предпочтительнее parentNode, поскольку он пропускает текстовые узлы и возвращает null, если родительский элемент не является элементом.
Соседние узлы
Представьте, что у вас есть список задач, где вы можете отмечать задачи как выполненные и переходить к следующей.
<!DOCTYPE html>
<html>
<head>
<title>Task List Navigation</title>
<style>
.task {
margin: 10px;
padding: 10px;
border: 1px solid #ccc;
}
.completed {
text-decoration: line-through;
color: gray;
}
</style>
</head>
<body>
<div class="task-list">
<div class="task">
<p>Task 1: Do the laundry</p>
<button class="complete-task">Complete Task</button>
</div>
<div class="task">
<p>Task 2: Buy groceries</p>
<button class="complete-task">Complete Task</button>
</div>
<div class="task">
<p>Task 3: Clean the house</p>
<button class="complete-task">Complete Task</button>
</div>
</div>
<script>
document.querySelectorAll('.complete-task').forEach(button => {
button.addEventListener('click', () => {
const task = button.parentElement;
task.classList.add('completed');
button.disabled = true;
const nextTask = task.nextElementSibling;
if (nextTask) {
console.log(`Next task: ${nextTask.querySelector('p').textContent}`);
} else {
console.log('No more tasks available');
}
});
});
</script>
</body>
</html>Этот код предоставляет список задач, где каждая задача имеет кнопку «Complete Task». Когда задача отмечается как выполненная, текст перечёркивается и кнопка становится неактивной. Также отображается описание следующей задачи. Если задач больше нет, выводится соответствующее сообщение. Аналогично, previousElementSibling и nextElementSibling пропускают текстовые узлы, что делает их безопаснее для обхода только элементов по сравнению с previousSibling и nextSibling.
Продвинутые техники обхода
Поиск элементов по классу или тегу
Представьте, что вы создаёте панель управления со списком всех пользователей и хотите найти и подсчитать все элементы пользователей.
<!DOCTYPE html>
<html>
<head>
<title>Finding Elements by Class or Tag</title>
</head>
<body>
<div class="user">User 1</div>
<div class="user">User 2</div>
<div class="user">User 3</div>
<script>
const users = document.getElementsByClassName('user');
// Display the number of users
console.log(`Number of users: ${users.length}`);
</script>
</body>
</html>Этот код подсчитывает и отображает количество элементов с классом user. Тонкая, но важная деталь: getElementsByClassName (и getElementsByTagName) возвращает живую HTMLCollection — она автоматически обновляется при изменении DOM. Если позже вы добавите четвёртый элемент .user, users.length станет 4 без повторного запроса. querySelectorAll, напротив, возвращает статический NodeList — снимок, сделанный в момент вызова. Сравните оба подхода в Поиск: getElement* и querySelector.
Методы Query Selector
Представьте, что у вас есть новостной сайт и вы хотите выделить все заголовки.
<!DOCTYPE html>
<html>
<head>
<title>Query Selector Methods</title>
</head>
<body>
<div id="news">
<h1 class="headline">Headline 1</h1>
<h1 class="headline">Headline 2</h1>
<h1 class="headline">Headline 3</h1>
</div>
<script>
const headlines = document.querySelectorAll('.headline');
// Highlight all headlines
headlines.forEach(headline => {
headline.style.color = 'red';
});
// Display the number of headlines
console.log(`Number of headlines: ${headlines.length}`);
</script>
</body>
</html>Этот код выбирает все элементы с классом headline, меняет их цвет на красный и отображает количество этих элементов.
Обход с помощью рекурсивных функций
Давайте создадим реальный пример рекурсивного обхода. Мы воспользуемся системой вложенных комментариев в качестве примера, где каждый комментарий может иметь ответы.
<!DOCTYPE html>
<html>
<head>
<title>Recursive Traversal</title>
<style>
.comment {
margin: 10px;
padding: 10px;
border: 1px solid #ccc;
}
.reply {
margin-left: 20px;
border-left: 2px solid #aaa;
}
</style>
</head>
<body>
<div class="comments">
<div class="comment">
<p>Comment 1</p>
<div class="reply">
<p>Reply 1-1</p>
<div class="reply">
<p>Reply 1-1-1</p>
</div>
</div>
<div class="reply">
<p>Reply 1-2</p>
</div>
</div>
<div class="comment">
<p>Comment 2</p>
<div class="reply">
<p>Reply 2-1</p>
</div>
</div>
</div>
<script>
function traverseComments(node) {
if (!node) return; // Guard against null/undefined
if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains('comment')) {
console.log(`Comment: ${node.querySelector('p').textContent}`);
}
if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains('reply')) {
console.log(`Reply: ${node.querySelector('p').textContent}`);
}
for (let i = 0; i < node.childNodes.length; i++) {
traverseComments(node.childNodes[i]);
}
}
traverseComments(document.querySelector('.comments'));
</script>
</body>
</html>Этот код представляет систему вложенных комментариев с комментариями и ответами. Функция traverseComments рекурсивно обходит каждый комментарий и ответ, отображая их текстовое содержимое. Вложенная структура позволяет добавлять ответы на ответы, демонстрируя реальный сценарий использования рекурсивного обхода. Всегда добавляйте проверку на null/undefined в начале рекурсивных функций, чтобы предотвратить ошибки, когда начальный селектор ничего не возвращает.
Практические примеры
Эти примеры сочетают обход DOM с распространёнными техниками манипуляций, демонстрируя реальные рабочие процессы.
Создание динамического списка задач
Представьте, что у вас есть список задач, куда пользователи могут добавлять новые задачи.
<!DOCTYPE html>
<html>
<head>
<title>Dynamic To-Do List</title>
<style>
.info { color: darkgreen; }
</style>
</head>
<body>
<div id="todo-list">
<h2>To-Do List</h2>
<ul id="tasks">
<li>Task 1</li>
<li>Task 2</li>
</ul>
<input type="text" id="task-input" placeholder="Add a new task" />
<button id="add-button">Add Task</button>
</div>
<script>
const tasks = document.getElementById('tasks');
const input = document.getElementById('task-input');
const button = document.getElementById('add-button');
button.addEventListener('click', () => {
const newTask = input.value.trim();
if (newTask) {
const li = document.createElement('li');
li.textContent = newTask;
tasks.appendChild(li);
input.value = '';
console.log('Added new task to the to-do list');
}
});
</script>
</body>
</html>Этот код позволяет пользователям добавлять новые задачи в список, вводя текст в поле ввода и нажимая кнопку.
Обновление атрибутов элементов
Представьте, что у вас есть список товаров и вы хотите отмечать товары как «избранные» при нажатии на них.
<!DOCTYPE html>
<html>
<head>
<title>Updating Element Attributes</title>
<style>
.favorite { font-weight: bold; color: gold; }
.info { color: darkblue; }
</style>
</head>
<body>
<h4>Click on the list item below to see the result!</h4>
<ul id="product-list">
<li>Product 1</li>
<li>Product 2</li>
<li>Product 3</li>
</ul>
<script>
const productList = document.getElementById('product-list');
productList.addEventListener('click', (event) => {
if (event.target.tagName === 'LI') {
event.target.classList.toggle('favorite');
console.log(`Toggled favorite status for: ${event.target.textContent}`);
}
});
</script>
</body>
</html>Этот код позволяет пользователям отмечать товары как «избранные», нажимая на них, изменяя их внешний вид с помощью класса favorite.
Минимизируйте обращения к DOM для повышения производительности. Группируйте операции с DOM, чтобы уменьшить количество перерисовок и перекомпоновок.
Распространённые ошибки
Несколько ловушек являются причиной большинства ошибок при обходе DOM:
- Текстовые узлы с пробелами.
firstChild,nextSiblingиchildNodesсчитают пробелы и переносы строк между тегами текстовыми узлами. Первый дочерний элемент<ul>, написанного в несколько строк, обычно является текстовым узлом, а не первым<li>. Используйте версии только для элементов (firstElementChild,nextElementSibling,children), если вам специально не нужны текстовые узлы. - Забывают, что обход может вернуть
null.parentElement,nextElementSiblingи аналогичные свойства возвращаютnullна краях дерева (у последнего соседнего элемента нетnextElementSibling). Всегда проверяйте результат перед вызовом метода на нём, как это делается в примере с соседними элементами черезif (nextTask). - Обращение с коллекциями как с массивами.
children,childNodesиgetElementsByClassNameвозвращают коллекции, а не настоящие массивы. УHTMLCollectionнет методаforEach. Преобразуйте с помощьюArray.from(collection)или[...collection], когда вам нужны методы массива, такие какmapилиfilter. (NodeListотquerySelectorAllимеетforEach, но неmap.) - Итерация по живой коллекции во время её изменения. Поскольку
getElementsByClassNameявляется живой коллекцией, добавление или удаление соответствующих элементов внутри циклаforпо ней может привести к пропуску элементов или бесконечному циклу. Сначала создайте снимок с помощьюArray.from(...), если планируете изменять коллекцию во время итерации.
Чтобы глубже изучить, как типизируются узлы и как выглядит их содержимое, прочитайте Свойства узлов: тип, тег и содержимое. Чтобы реагировать на действия пользователя при обходе, смотрите Обработка событий в DOM.
Заключение
Освоение обхода DOM необходимо для создания динамических, интерактивных веб-приложений. Понимая и используя различные методы и техники навигации и манипуляции с DOM, вы можете улучшить пользовательский опыт и повысить функциональность своих веб-проектов.