querySelector в JavaScript: поиск элементов без хрупких селекторов

JS / DOM и события

querySelector в JavaScript: поиск элементов без хрупких селекторов

Как пользоваться querySelector и querySelectorAll, ограничивать область поиска, проверять null и безопасно строить селекторы.

Опубликовано 04.09.2026 4 минуты чтения
Содержание статьи

querySelector() возвращает первый элемент, подходящий под CSS-селектор, а querySelectorAll() — статический список всех совпадений. Эти методы позволяют использовать знакомый синтаксис классов, атрибутов и псевдоклассов.

JavaScript
const heading = document.querySelector('h1');const cards = document.querySelectorAll('.card');​

Если совпадения нет, первый метод возвращает null, второй — пустой NodeList.

Основные селекторы

JavaScript
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 явно сообщает о поведении.

HTML
<button type="button" data-menu-button aria-expanded="false">Меню</button>​

Всегда учитывайте null

Небезопасно:

JavaScript
document.querySelector('[data-menu-button]').addEventListener('click', openMenu);​

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

JavaScript
const menuButton = document.querySelector('[data-menu-button]');​if (menuButton) {  menuButton.addEventListener('click', openMenu);}​

Optional chaining подходит для простого единичного действия:

JavaScript
document.querySelector('[data-close]')?.addEventListener('click', closeDialog);​

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

JavaScript
function requireElement(selector, root = document) {  const element = root.querySelector(selector);​  if (!element) {    throw new Error(`Не найден обязательный элемент: ${selector}`);  }​  return element;}​

Ограничивайте область компонента

Глобальный поиск может выбрать элемент из другой карточки:

HTML
<article class="counter" data-counter>  <output data-counter-value>0</output>  <button type="button" data-counter-increase>Увеличить</button></article>​

JavaScript
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() возвращает статический список

JavaScript
const items = document.querySelectorAll('.list__item');const list = document.querySelector('.list');​list?.append(document.createElement('li'));console.log(items.length); // Старое количество.​

Новый элемент не появится в уже полученном NodeList. При необходимости выполните запрос снова. Это отличается от некоторых живых HTML-коллекций.

NodeList поддерживает forEach и итерацию:

JavaScript
for (const item of document.querySelectorAll('.list__item')) {  item.classList.add('is-ready');}​

Для методов массива создайте массив:

JavaScript
const labels = [...document.querySelectorAll('[data-label]')]  .map((element) => element.textContent.trim());​

matches() и closest()

matches() проверяет сам элемент, closest() идёт вверх по предкам:

JavaScript
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 для прямых потомков

JavaScript
const directItems = list?.querySelectorAll(':scope > li') ?? [];​

:scope обозначает корень текущего запроса. Без него селектор с > нельзя надёжно привязать к выбранному контейнеру.

Динамические значения и CSS.escape()

Не вставляйте произвольную строку в селектор:

JavaScript
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().

Сообщить об ошибке в статье

Рекомендации редакции

Другие публикации из этого раздела.

Новые материалы

Получайте практические разборы без информационного шума

Одна общая рассылка о новых статьях и инструментах. Адрес будет активироваться только после подтверждения.

Подписка появится после подключения сервиса рассылок и Yandex SmartCaptcha.