W3docs

JavaScript — обход DOM

Обход DOM (Document Object Model) — фундаментальный навык для веб-разработчиков. Освоив его, вы сможете динамически управлять страницами.

Обход DOM (Document Object Model) — фундаментальный навык для веб-разработчиков, работающих с JavaScript. Освоив обход DOM, вы сможете динамически управлять веб-страницами, создавая интерактивные и адаптивные интерфейсы. Это руководство предоставит вам подробные объяснения и множество примеров кода, которые помогут вам стать профессионалом в данной области.

Введение в обход DOM

DOM представляет структуру веб-страницы в виде дерева узлов. Каждый узел соответствует элементу, фрагменту текста или комментарию на странице. Обход DOM означает перемещение от одного узла к другому — вверх к родительскому, вниз к дочерним или горизонтально к соседним — чтобы читать или изменять элементы относительно начальной точки.

Зачем обходить DOM вместо того, чтобы просто делать выборку? Нередко вы начинаете с элемента, который у вас уже есть (например, кнопка, которую нажал пользователь), и вам нужно добраться до связанного элемента, точный id или класс которого вам заранее неизвестен — его контейнера, следующего элемента в списке или всех вложенных ответов под ним. Обход выражает идею «то, что находится рядом с / внутри / вокруг этого элемента».

В этом руководстве рассматриваются наиболее часто используемые свойства отношений:

НаправлениеСвойство только для элементовСвойство, включающее всё
Вниз (дочерние)children, firstElementChild, lastElementChildchildNodes, firstChild, lastChild
Вверх (родительский)parentElementparentNode
Горизонтально (соседние)nextElementSibling, previousElementSiblingnextSibling, 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. Тонкая, но важная деталь: getElementsByClassNamegetElementsByTagName) возвращает живую 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, вы можете улучшить пользовательский опыт и повысить функциональность своих веб-проектов.

Практика

Практика
Какие из следующих методов можно использовать для обхода DOM в JavaScript?
Какие из следующих методов можно использовать для обхода DOM в JavaScript?
Was this page helpful?