JavaScript: поиск элементов — getElement*, querySelector*
Изучаем методы поиска элементов в DOM: getElementById, getElementsByClassName, querySelector и querySelectorAll — с примерами и объяснением разницы между живыми и статическими коллекциями.
Прежде чем что-то изменить, переместить или прочитать на странице, нужно найти нужный элемент. Это самый первый шаг практически в любой задаче с DOM, и JavaScript предоставляет для этого два семейства инструментов:
- Устаревшие методы
getElement*—getElementById,getElementsByClassName,getElementsByTagName. Они быстры и возвращают живые коллекции. - Современные методы
querySelector*—querySelector,querySelectorAll. Они принимают любой CSS-селектор и возвращают статические результаты.
В этом руководстве рассматриваются оба семейства, вспомогательные методы matches, closest и contains для проверки и обхода дерева, а также самый важный подводный камень: разница между живой коллекцией и статической. Каждый пример можно запустить и сразу увидеть результат.
Когда вы научитесь находить элементы, следующими шагами обычно становятся обход DOM для перехода к соседним элементам и манипуляции с DOM для его изменения. Для общего знакомства смотрите раздел выбор элементов DOM.
Эффективный доступ к элементам: getElementById
Метод getElementById — самый быстрый и надёжный способ получить один элемент, поскольку идентификатор (ID) должен быть уникальным в документе, а браузеры индексируют их внутренне. Он возвращает найденный элемент или null, если элемента с таким ID не существует — поэтому перед использованием результата необходимо проверять его на null. Обратите внимание, что вы передаёте голый ID, без ведущего символа # (он используется только в CSS-селекторах). В примере ниже исходный текст «Default text» сразу же заменяется.
<!-- snippet: html-result -->
<!DOCTYPE html>
<html>
<head>
<title>getElementById Example</title>
</head>
<body>
<div id="main-content">Default text</div>
<script>
const element = document.getElementById('main-content');
element.innerHTML = "Modified text!"
</script>
</body>
</html>Доступ к нескольким элементам: getElementsByClassName и getElementsByTagName
При выборе элементов по имени класса или имени тега вы получаете обратно HTMLCollection. Это живая коллекция: она автоматически обновляется при изменении DOM. Она похожа на массив — можно обращаться к элементам по индексу (els[0]) и проверять els.length — но это не настоящий массив, поэтому у неё нет forEach, map или filter. Для безопасного перебора сначала преобразуйте её с помощью Array.from(...) (или оператора расширения [...els]).
Пример с getElementsByClassName
Для доступа к нескольким элементам с одним и тем же классом используйте getElementsByClassName. В этом примере у нас есть два элемента div с одинаковым именем класса. Мы изменяем оба, выбирая их по имени класса.
<!-- snippet: html-result -->
<!DOCTYPE html>
<html>
<head>
<title>getElementsByClassName Example</title>
</head>
<body>
<div class="info">First Info</div>
<div class="info">Second Info</div>
<script>
const infoElements = document.getElementsByClassName('info');
Array.from(infoElements).forEach(el => el.innerHTML = "MODIFIED!");
</script>
</body>
</html>Пример с getElementsByTagName
Получайте элементы по имени тега с помощью getElementsByTagName. Это полностью аналогично предыдущему примеру, но на этот раз мы выбираем по имени тега, а не по имени класса.
<!-- snippet: html-result -->
<!DOCTYPE html>
<html>
<head>
<title>getElementsByTagName Example</title>
</head>
<body>
<p>First Paragraph</p>
<p>Second Paragraph</p>
<script>
const paragraphs = document.getElementsByTagName('p');
Array.from(paragraphs).forEach(el => el.innerHTML = "MODIFIED!");
</script>
</body>
</html>Гибкий поиск с querySelector и querySelectorAll
Выбор с помощью querySelector
Используйте querySelector для поиска первого элемента, соответствующего CSS-селектору. В этом примере мы выбираем первый элемент с классом text, который является прямым потомком элемента с id main.
<!-- snippet: html-result -->
<!DOCTYPE html>
<html>
<head>
<title>QuerySelector Example</title>
</head>
<body>
<div id="main"><span class="text">This will be replaced</span></div>
<div id="other"><span class="text">This one doesn't change</span></div>
<script>
const spanInsideDiv = document.querySelector('#main > .text');
spanInsideDiv.innerHTML = "MODIFIED!";
</script>
</body>
</html>Получение нескольких элементов с querySelectorAll
querySelectorAll возвращает все элементы, соответствующие CSS-селектору, в виде статического NodeList. Удобно то, что у NodeList есть встроенный forEach, поэтому можно перебирать его напрямую, без преобразования в массив.
Слово статический важно: querySelectorAll делает снимок совпадающих элементов в момент вызова. Если вы добавите или удалите совпадающие элементы после этого, снимок не изменится. Это прямая противоположность живой HTMLCollection, возвращаемой методами getElementsBy*.
<!-- snippet: html-result -->
<!DOCTYPE html>
<html>
<head>
<title>QuerySelectorAll Example</title>
</head>
<body>
<ul>
<li class="item">Item 1</li>
<li class="item">Item 2</li>
</ul>
<script>
const items = document.querySelectorAll('.item');
items.forEach(item => item.innerHTML = "MODIFIED!");
</script>
</body>
</html>Живые и статические коллекции: главный подводный камень
Это ловушка, в которую попадает большинство новичков. Живая HTMLCollection отражает текущее состояние DOM при каждом обращении, тогда как статический NodeList заморожен в момент выборки. Фрагмент ниже показывает, как оба реагируют на добавление нового элемента:
// Suppose the page has two <li class="item"> elements.
const live = document.getElementsByClassName('item'); // live HTMLCollection
const snapshot = document.querySelectorAll('.item'); // static NodeList
console.log(live.length); // 2
console.log(snapshot.length); // 2
// Now add a third matching element.
const li = document.createElement('li');
li.className = 'item';
document.querySelector('ul').appendChild(li);
console.log(live.length); // 3 — updated automatically
console.log(snapshot.length); // 2 — still the old snapshotПочему это важно: перебор живой коллекции с одновременным удалением совпадающих элементов — классический источник пропущенных элементов, потому что коллекция уменьшается прямо под вами. Статический NodeList от querySelectorAll безопаснее в таком случае, поскольку список не изменится в процессе перебора.
Проверка и обход: matches, closest и contains
Поиск — это не только нахождение элементов; зачастую у вас уже есть элемент и нужно задать о нём вопрос.
element.matches(selector)возвращаетtrue, если сам элемент соответствует CSS-селектору. Отлично подходит для делегирования событий.element.closest(selector)поднимается вверх по дереву от элемента (включая его самого) и возвращает ближайшего предка, соответствующего селектору, илиnull.parent.contains(node)возвращаетtrue, еслиnodeявляется самим родителем или его потомком.
<!-- snippet: html-result -->
<!DOCTYPE html>
<html>
<body>
<section class="card">
<button id="save" class="btn primary">Save</button>
</section>
<div id="out"></div>
<script>
const btn = document.getElementById('save');
const section = document.querySelector('.card');
const out = document.getElementById('out');
out.innerHTML =
'matches(".primary"): ' + btn.matches('.primary') + '<br>' +
'closest(".card") is section: ' + (btn.closest('.card') === section) + '<br>' +
'section.contains(btn): ' + section.contains(btn);
</script>
</body>
</html>Какой метод использовать?
- Нужен один элемент по ID? Используйте
getElementById— это самый быстрый и понятный способ. - Нужен CSS-селектор (потомки, комбинаторы, атрибуты,
:not())? ИспользуйтеquerySelector/querySelectorAll. - Нужен живой список, отслеживающий изменения DOM? Используйте
getElementsByClassName/getElementsByTagName. - Есть элемент и нужно проверить или подняться по дереву? Используйте
matches,closestилиcontains.
Заключение
Поиск элементов — основа любого скрипта для работы с DOM. Используйте getElementById для единственного ID, querySelector* для гибкости CSS-селекторов, а методы getElementsBy* — только тогда, когда вам действительно нужна живая коллекция. Главное — помните разницу между живыми и статическими коллекциями, чтобы они не застали вас врасплох в середине цикла. Далее переходите к разделам обход DOM и манипуляции с DOM.