querySelector и querySelectorAll
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
Браузер умеет искать элементы тем же языком, которым вы их оформляете — CSS-селектором. querySelector вернёт первый подходящий элемент, querySelectorAll — сразу весь список, и на этой паре держится почти вся работа со страницей.
Раньше на каждый способ поиска был свой метод: getElementById, getElementsByClassName, getElementsByTagName. Каждый умеет ровно одно и ничего не умеет комбинировать. Современный поиск устроен проще: вы передаёте строку CSS-селектора — ту же самую, что написали бы в стилях, — а браузер сам применяет её к документу.
querySelector обходит документ в порядке разметки и отдаёт первый подошедший элемент. Не самый вложенный и не самый подходящий по смыслу — просто первый сверху. Если совпадений нет, вы получите null: не пустой объект и не исключение. Поэтому обращение к свойству «найденного» элемента — типовое место падения, и ?. здесь выручает.
Селектор — это CSS, а не произвольная строка. Идентификатор "2col" в HTML совершенно законен, но querySelector("#2col") бросит SyntaxError: в CSS имя не может начинаться с цифры. getElementById("2col") при этом спокойно сработает — как и querySelector("#" + CSS.escape("2col")). Полчаса моей жизни ушло именно на это.
querySelectorAll собирает все совпадения и возвращает NodeList. Он похож на массив ровно настолько, насколько нужно для быстрого взгляда: есть length, есть доступ по индексу. Если не нашлось ничего, вернётся не null, а пустой список с length === 0 — и это удобно, потому что перебор пустого списка просто ничего не сделает.
Ключевое слово — снимок. NodeList из querySelectorAll статический: браузер один раз прошёл по документу, записал найденное и больше за страницей не следит. Добавили подходящий элемент — старая переменная об этом не узнает, нужен повторный вызов.
Оба метода есть не только у document, но и у любого элемента. elem.querySelectorAll(...) ищет только среди потомков elem: сам элемент в результат не попадает, соседи и родители — тем более. Это главное лекарство от случайных совпадений: сузили область — и селектор можно писать коротким.
Кажется, что elem.querySelector(...) применяет селектор «внутри» элемента. На самом деле браузер сопоставляет селектор со всем документом, а затем выбрасывает всё, что не является потомком elem. Отсюда странность: селектор может начинаться с предков элемента и всё равно сработает. Если нужно отсчитывать именно от элемента, помогает псевдокласс :scope.
У NodeList есть собственный forEach — с тем же интерфейсом, что у массива: элемент, индекс, коллекция. И он итерируемый, а значит работают for..of, спред и деструктуризация. В большинстве задач этого достаточно, и никакие преобразования не нужны.
for..of удобнее там, где нужны break или continue: из forEach так просто не выйти. Обычный for по индексам тоже работает — length и items[i] на месте, — но читается хуже и почти никогда не нужен.
NodeList — не массив, а отдельный класс. Он не наследует Array.prototype, поэтому map, filter, reduce, find, slice у него отсутствуют, а Array.isArray честно отвечает false. Причина историческая: DOM-коллекции появились раньше, чем эти методы у массивов, и живут в своей ветке спецификации.
| Возможность | NodeList | Массив |
|---|---|---|
| length | есть | есть |
| доступ по индексу items[0] | есть | есть |
| forEach | есть | есть |
| for..of, спред, деструктуризация | есть | есть |
| map / filter / reduce / find | нет | есть |
| Array.isArray(...) | false | true |
Чинится в одну строку: Array.from(items) или [...items] — оба дают настоящий массив, потому что NodeList итерируемый. У Array.from есть бонус: вторым аргументом принимается функция преобразования, и лишний проход по коллекции не понадобится.
Старые методы отдают HTMLCollection — не снимок, а запрос, который пересчитывается при каждом обращении: добавили элемент, и length вырос сам. Звучит удобно, но именно на этом ломаются циклы, которые в теле что-то добавляют или удаляют. И forEach у HTMLCollection нет. Подробно эти методы и их ловушки — в статье про getElementById и старые методы.
- querySelector и querySelectorAll принимают любой CSS-селектор — тот же, что вы пишете в стилях.
- querySelector отдаёт первый подходящий элемент в порядке разметки или null; querySelectorAll — NodeList, при неудаче пустой.
- NodeList из querySelectorAll статический: это снимок на момент вызова, изменения DOM в нём не появятся.
- Оба метода работают и на элементе, ограничивая результат его потомками, хотя сам селектор сопоставляется со всем документом — для отсчёта от элемента есть :scope.
- NodeList итерируемый и имеет forEach, но не map и не filter: нужен массив — Array.from(items) или [...items].
1/3document.querySelector(".missing") не нашёл ни одного элемента. Что он вернёт?
2/3Список получен через const items = document.querySelectorAll("li"), после чего в документ добавили ещё один li. Чему равен items.length?
3/3Почему items.map(...) на результате querySelectorAll падает с TypeError?
Сумма только доступных товаров
В корзине три позиции, но в наличии лишь часть — у них класс in-stock. Посчитайте сумму data-price только по доступным и выведите её. Постарайтесь отфильтровать не циклом с if, а самим селектором.
Условие «в наличии» переносим прямо в селектор — тогда фильтровать в JavaScript уже нечего. NodeList превращаем в массив спредом и складываем через reduce.
Без превращения в массив тоже нормально: let sum = 0; for (const li of items) sum += Number(li.dataset.price); — NodeList итерируемый, этого достаточно.
Только внутри второй карточки
На странице две карточки, в каждой свои теги с классом tag. Соберите массив строк с текстами тегов второй карточки — так, чтобы теги первой в него не попали.
Сначала берём вторую карточку по индексу, затем ищем теги на ней, а не на документе. Array.from сразу превращает NodeList в массив строк вторым аргументом.
Главное здесь — вызов querySelectorAll на элементе: он ограничивает поиск потомками карточки. Вариант [...second.querySelectorAll(".tag")].map((el) => el.textContent) делает то же самое за два прохода.