getElementById и старые методы
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
Метод document.getElementById старше jQuery и переживёт ещё не одну моду — как и его соседи getElementsByClassName и getElementsByTagName. Разберём эту троицу и её главную особенность: живые коллекции, которые меняются прямо во время перебора.
Если у элемента есть атрибут id, брать его нужно этим методом. Он возвращает либо сам элемент, либо null, если ничего не нашлось. Исключения не будет — проверка результата целиком на вашей совести, и обращение к свойству несуществующего элемента упадёт уже строкой ниже.
Есть и побочный эффект, о котором полезно знать: для каждого id браузер заводит одноимённое свойство у window. Работает во всех браузерах — и всё равно так писать не стоит: любая ваша переменная с тем же именем перекроет это свойство, а читатель кода будет гадать, откуда взялась переменная banner.
getElementById живёт только на document: у элемента такого метода нет. Логика простая — id уникален на всю страницу, искать его внутри куска разметки бессмысленно.
Когда элементов много, работают два соседних метода: getElementsByClassName ищет по классу, getElementsByTagName — по имени тега ("*" означает «любой»). Оба доступны и у document, и у любого элемента: вызов на элементе ограничивает поиск его потомками. Если в класс передать несколько имён через пробел, найдутся элементы, у которых есть все перечисленные классы; порядок не важен.
Возвращают они не массив, а HTMLCollection. Индексы и length есть, for..of работает, а вот map, filter и forEach — нет. Нужны методы массива — оберните в Array.from.
Звёздочка собирает всех потомков подряд, а несколько классов в строке — это «И», а не «ИЛИ», причём порядок слов роли не играет.
Вот главное отличие старых методов от querySelectorAll: их коллекции живые. Это не снимок, сделанный в момент вызова, а постоянно действующий запрос к документу. Появился подходящий элемент — он сам возник в коллекции; исчез — сам пропал. Повторно вызывать метод не нужно.
Пока вы только читаете length, живость — удобство. Ловушка начинается, когда цикл сам меняет документ: коллекция пересчитывается после каждого шага, индексы съезжают, и часть элементов остаётся необработанной. Классика жанра — перенос элементов из одного контейнера в другой циклом по возрастающему индексу.
Array.from делает снимок: дальше вы перебираете обычный массив, и что происходит с документом, ему уже безразлично.
Соблазн заменить всё на querySelector понятен: один синтаксис вместо трёх методов. Но пара причин оставить старое в живых есть. Первая — getElementById ищет по идентификатору, а не по CSS-селектору, и ему безразлично, что внутри id: точки, двоеточия, скобки. Селектору не безразлично.
Вторая причина — та самая живость, когда она нужна: держите коллекцию в переменной, и её length всегда актуален без повторного поиска. Про скорость думать не стоит: разница между методами заметна разве что в синтетическом тесте на десятки тысяч вызовов.
| Метод | Где доступен | Что возвращает | Живая |
|---|---|---|---|
| getElementById | только document | элемент или null | — |
| getElementsByClassName | document и элемент | HTMLCollection | да |
| getElementsByTagName | document и элемент | HTMLCollection | да |
| getElementsByName | только document | NodeList | да |
| querySelectorAll | document и элемент | NodeList | нет |
Практическое правило: есть id — getElementById; нужна живая коллекция — getElementsBy*; во всех остальных случаях querySelector и querySelectorAll из прошлой статьи.
- getElementById есть только у document, возвращает элемент или null и не требует экранирования спецсимволов в id — в отличие от селектора.
- getElementsByClassName и getElementsByTagName работают и на элементе, ограничивая поиск его потомками; несколько классов через пробел означают «все сразу».
- Обе коллекции — HTMLCollection: есть индексы, length и for..of, но нет map, filter и forEach; методы массива — через Array.from.
- Живая коллекция — это запрос, а не снимок: меняя документ внутри цикла по индексам, вы пропустите часть элементов.
- По умолчанию берите querySelector; старые методы выигрывают, когда есть id или когда живость коллекции нужна специально.
1/3В блоке лежат два элемента с классом x. Код берёт живую коллекцию и циклом for (let i = 0; i < live.length; i++) переносит live[i] в соседний блок. Сколько элементов окажется в новом блоке?
2/3Что найдёт document.getElementsByClassName("card hot")?
3/3Что вернёт document.getElementById("нет-такого")?
Письма застревают в ящике
В блоке #inbox три письма с классом mail. Код должен перенести их все в #archive, но переносит не все. Предположите, что напечатает console.log, объясните почему и почините код, не отказываясь от getElementsByClassName.
Напечатает 2 и 13. Коллекция живая: как только письмо переехало в архив, оно исчезло из mails, а остальные сдвинулись на позицию влево. На шаге i = 0 уходит первое письмо, на шаге i = 1 — уже третье (второе к этому моменту стоит под индексом 0), а на i = 2 длина равна 1, и цикл заканчивается.
Лечится снимком коллекции:
Второй вариант — наоборот, опереться на живость и всегда брать нулевой элемент: while (mails.length) archive.append(mails[0]);. Работает и перебор индексов с конца, но снимок читается яснее всего.
Ссылки только в подвале
На странице две группы ссылок. Посчитайте ссылки только внутри #footer, не задев шапку. Подсказка: искать необязательно от document.
getElementsByTagName есть не только у document, но и у любого элемента — и тогда он ищет только среди его потомков.
Тот же результат даст document.querySelectorAll("#footer a").length. Разница в том, что первая коллекция живая: если в подвал позже добавят ссылку, её length обновится сам, а у второй — нет.