JS / DOM и события
querySelector в JavaScript: поиск элементов без хрупких селекторов
Как пользоваться querySelector и querySelectorAll, ограничивать область поиска, проверять null и безопасно строить селекторы.
Содержание статьи
querySelector() возвращает первый элемент, подходящий под CSS-селектор, а querySelectorAll() — статический список всех совпадений. Эти методы позволяют использовать знакомый синтаксис классов, атрибутов и псевдоклассов.
const heading = document.querySelector('h1');const cards = document.querySelectorAll('.card');Если совпадения нет, первый метод возвращает null, второй — пустой NodeList.
Основные селекторы
document.querySelector('#profile');document.querySelector('.card');document.querySelector('[data-menu-button]');document.querySelector('input[type="email"]');document.querySelector('.article > h2');document.querySelector('.tabs [aria-selected="true"]');Для JavaScript-хуков удобны data-* атрибуты. Класс может измениться из-за дизайна, а data-menu-button явно сообщает о поведении.
<button type="button" data-menu-button aria-expanded="false">Меню</button>Всегда учитывайте null
Небезопасно:
document.querySelector('[data-menu-button]').addEventListener('click', openMenu);Если компонента нет на текущей странице, код завершится ошибкой. Проверяйте результат:
const menuButton = document.querySelector('[data-menu-button]');if (menuButton) { menuButton.addEventListener('click', openMenu);}Optional chaining подходит для простого единичного действия:
document.querySelector('[data-close]')?.addEventListener('click', closeDialog);Если элемент обязателен для работы модуля, полезнее выбросить понятную ошибку во время разработки:
function requireElement(selector, root = document) { const element = root.querySelector(selector); if (!element) { throw new Error(`Не найден обязательный элемент: ${selector}`); } return element;}Ограничивайте область компонента
Глобальный поиск может выбрать элемент из другой карточки:
<article class="counter" data-counter> <output data-counter-value>0</output> <button type="button" data-counter-increase>Увеличить</button></article>const counters = document.querySelectorAll('[data-counter]');for (const counter of counters) { const output = counter.querySelector('[data-counter-value]'); const button = counter.querySelector('[data-counter-increase]'); if (!output || !button) { continue; } button.addEventListener('click', () => { const current = Number.parseInt(output.value, 10) || 0; output.value = String(current + 1); });}Поиск от корня компонента делает несколько экземпляров независимыми.
querySelectorAll() возвращает статический список
const items = document.querySelectorAll('.list__item');const list = document.querySelector('.list');list?.append(document.createElement('li'));console.log(items.length); // Старое количество.Новый элемент не появится в уже полученном NodeList. При необходимости выполните запрос снова. Это отличается от некоторых живых HTML-коллекций.
NodeList поддерживает forEach и итерацию:
for (const item of document.querySelectorAll('.list__item')) { item.classList.add('is-ready');}Для методов массива создайте массив:
const labels = [...document.querySelectorAll('[data-label]')] .map((element) => element.textContent.trim());matches() и closest()
matches() проверяет сам элемент, closest() идёт вверх по предкам:
const list = document.querySelector('[data-action-list]');list?.addEventListener('click', (event) => { const button = event.target.closest('button[data-action]'); if (!button || !list.contains(button)) { return; } if (button.matches('[data-action="remove"]')) { button.closest('li')?.remove(); }});Проверка contains() важна, если делегирующий контейнер имеет вложенные компоненты или closest() может выйти за ожидаемую границу.
:scope для прямых потомков
const directItems = list?.querySelectorAll(':scope > li') ?? [];:scope обозначает корень текущего запроса. Без него селектор с > нельзя надёжно привязать к выбранному контейнеру.
Динамические значения и CSS.escape()
Не вставляйте произвольную строку в селектор:
const unsafeId = 'section:1';const element = document.querySelector(`#${CSS.escape(unsafeId)}`);CSS.escape() делает значение безопасным для синтаксиса селектора. Если у вас уже есть идентификатор, иногда проще document.getElementById(unsafeId): он не требует CSS-экранирования.
Селектор, полученный целиком от пользователя, может быть синтаксически неверным и вызвать DOMException. Не принимайте такие строки без жёсткого allowlist.
Производительность без преждевременной оптимизации
Для обычной страницы разница между простыми селекторами редко важнее читаемости. Полезные привычки:
- найдите корень компонента один раз;
- не выполняйте один запрос внутри каждого кадра анимации;
- сохраняйте часто используемый элемент в переменной;
- избегайте селекторов, завязанных на длинную цепочку DOM;
- профилируйте реальную проблему до оптимизации.
Что запомнить
querySelector() возвращает элемент или null, а querySelectorAll() — статический список. Ищите внутренние узлы от корня компонента, используйте устойчивые data-* хуки, применяйте closest() для делегирования и экранируйте динамические части селектора через CSS.escape().