1. Что возвращают методы поиска элементов?
Когда мы используем методы поиска элементов на странице, результат зависит от того, какой именно метод мы выбрали:
- getElementById — всегда возвращает один элемент (или null, если не найден).
- getElementsByClassName и getElementsByTagName — возвращают коллекцию (HTMLCollection или NodeList), то есть некий "псевдомассив" элементов.
- querySelector — возвращает первый найденный элемент (или null).
- querySelectorAll — возвращает коллекцию (NodeList) всех подходящих элементов.
Вот пример:
<ul id="fruits">
<li class="fruit">Яблоко</li>
<li class="fruit">Груша</li>
<li class="fruit">Банан</li>
</ul>
// Получим коллекцию всех элементов с классом 'fruit'
const fruits = document.getElementsByClassName('fruit');
// Получим NodeList всех элементов li внутри #fruits
const fruitItems = document.querySelectorAll('#fruits li');
Вопрос: А можно ли с этими коллекциями работать как с обычными массивами? Вот тут-то и начинаются интересности!
2. Коллекции и массивы: в чём разница?
В JavaScript есть два типа коллекций, которые часто возвращают методы DOM:
- HTMLCollection — возвращается, например, из getElementsByClassName и getElementsByTagName.
- NodeList — возвращается из querySelectorAll (и иногда из других методов).
Они очень похожи: оба содержат набор элементов, оба поддерживают доступ по индексу (fruits[0]), у обоих есть свойство length. Но это не совсем массивы! А вот и таблица для сравнения:
| Свойство/Метод | Массив (Array) | HTMLCollection | NodeList |
|---|---|---|---|
| Индексация | ✅ | ✅ | ✅ |
| length | ✅ | ✅ | ✅ |
| forEach | ✅ | ❌ | ✅ (NodeList) |
| map, filter, reduce | ✅ | ❌ | ❌ |
| push, pop | ✅ | ❌ | ❌ |
| Живой (live) | ❌ | ✅ | ❌ (обычно) |
| Итерация for...of | ✅ | ✅ | ✅ |
Что значит "живой" (live)?
HTMLCollection "живая" — если вы добавите или удалите элемент с подходящим классом, коллекция обновится сама. NodeList обычно "неживой" (static): если вы нашли элементы, а потом добавили новый — он не появится в этой коллекции.
3. Перебор коллекций: разные подходы
Старый, но надёжный способ: цикл for
Поскольку и HTMLCollection, и NodeList поддерживают индексацию, мы можем использовать обычный цикл for:
const fruits = document.getElementsByClassName('fruit');
for (let i = 0; i < fruits.length; i++) {
fruits[i].style.color = 'green'; // Покрасим все фрукты в зелёный
}
Современный подход: for...of
Почти все коллекции DOM поддерживают перебор через for...of (начиная с ES6):
const fruitItems = document.querySelectorAll('#fruits li');
for (const item of fruitItems) {
item.style.fontWeight = 'bold'; // Сделаем все фрукты жирными
}
forEach: да или нет?
NodeList, возвращаемый из querySelectorAll, поддерживает метод forEach:
const fruitItems = document.querySelectorAll('#fruits li');
fruitItems.forEach(function(item) {
item.textContent = item.textContent + ' 🍏';
});
Но! HTMLCollection не поддерживает forEach напрямую. Если вы попробуете:
const fruits = document.getElementsByClassName('fruit');
fruits.forEach(function(item) { /* ... */ }); // Ошибка!
Браузер обидится: "fruits.forEach is not a function".
4. Как превратить коллекцию в массив
Если вам нужно использовать методы массива (map, filter, forEach и т.д.), можно превратить коллекцию в настоящий массив. Для этого есть несколько способов:
Способ 1: Array.from
const fruits = document.getElementsByClassName('fruit');
const fruitsArray = Array.from(fruits);
fruitsArray.forEach(item => {
item.style.background = 'lightyellow';
});
Способ 2: spread-оператор (...)
const fruits = document.getElementsByClassName('fruit');
const fruitsArray = [...fruits];
fruitsArray.map(item => {
item.textContent = item.textContent.toUpperCase();
});
Способ 3: старый добрый Array.prototype.slice
const fruits = document.getElementsByClassName('fruit');
const fruitsArray = Array.prototype.slice.call(fruits);
fruitsArray.filter(item => item.textContent.includes('Яблоко'))
.forEach(item => item.style.border = '2px solid red');
5. Практика: меняем стили и содержимое всех элементов
Давайте добавим к нашему учебному приложению функционал, который подсвечивает все пункты списка, если пользователь нажимает на кнопку.
HTML:
<ul id="fruits">
<li class="fruit">Яблоко</li>
<li class="fruit">Груша</li>
<li class="fruit">Банан</li>
</ul>
<button id="highlight">Подсветить все</button>
JavaScript:
const btn = document.getElementById('highlight');
const fruits = document.getElementsByClassName('fruit');
btn.addEventListener('click', function() {
// Перебираем коллекцию через обычный for
for (let i = 0; i < fruits.length; i++) {
fruits[i].style.background = 'lightgreen';
}
});
Теперь при нажатии на кнопку все пункты подсвечиваются зелёным!
А если хочется использовать forEach?
btn.addEventListener('click', function() {
// Преобразуем коллекцию в массив и используем forEach
Array.from(fruits).forEach(item => {
item.style.background = 'lightgreen';
});
});
6. Пример: динамическое добавление элементов и обновление коллекции
Давайте усложним наше приложение. Пусть пользователь может добавлять новый фрукт в список, а кнопка "Подсветить все" всегда подсвечивает все пункты, включая новые.
HTML:
<ul id="fruits">
<li class="fruit">Яблоко</li>
<li class="fruit">Груша</li>
<li class="fruit">Банан</li>
</ul>
<input id="new-fruit" placeholder="Новый фрукт">
<button id="add-fruit">Добавить</button>
<button id="highlight">Подсветить все</button>
JavaScript:
const fruitsList = document.getElementById('fruits');
const input = document.getElementById('new-fruit');
const addBtn = document.getElementById('add-fruit');
const highlightBtn = document.getElementById('highlight');
addBtn.addEventListener('click', function() {
const value = input.value.trim();
if (value) {
const li = document.createElement('li');
li.className = 'fruit'; // не забываем класс!
li.textContent = value;
fruitsList.appendChild(li);
input.value = '';
}
});
// Теперь кнопка подсвечивает все элементы, включая добавленные
highlightBtn.addEventListener('click', function() {
// fruits — всегда актуальная HTMLCollection!
const fruits = document.getElementsByClassName('fruit');
for (let i = 0; i < fruits.length; i++) {
fruits[i].style.background = 'lightgreen';
}
});
Важно:
Поскольку getElementsByClassName возвращает "живую" коллекцию, новые элементы автоматически попадают в неё. Если бы мы использовали querySelectorAll, пришлось бы каждый раз заново искать элементы.
7. Типичные ошибки при переборе коллекций и массивов
Ошибка №1: попытка использовать методы массива на коллекции напрямую.
Многие новички уверены, что если коллекция выглядит как массив, значит к ней можно применить map или forEach. Но для HTMLCollection это не так — получите "is not a function". Не забывайте преобразовывать коллекцию в массив!
Ошибка №2: путаница между живыми и неживыми коллекциями.
Добавили новый элемент, а он не появляется в NodeList? Или, наоборот, цикл по живой коллекции зациклился, потому что вы добавляете элементы внутри этого же цикла? Всегда учитывайте, как ведёт себя ваша коллекция.
Ошибка №3: забыли про индексацию с нуля.
И HTMLCollection, и NodeList, и массивы в JavaScript индексируются с нуля. Если начинаете с 1, потеряете первый элемент.
Ошибка №4: изменение коллекции во время перебора.
Если вы добавляете или удаляете элементы внутри цикла по живой коллекции, может возникнуть путаница: элементы перескакивают, длина меняется, часть элементов не обрабатывается. В таких случаях лучше сначала собрать массив нужных элементов, а потом работать с ним.
Ошибка №5: не проверяете, что коллекция не пуста.
Иногда методы поиска возвращают пустую коллекцию. Если сразу начать работать с fruits[0], получите undefined и, скорее всего, ошибку.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ