1. Почему так легко ошибиться при работе с DOM?
Работа с DOM — это как прогулка по тёмному коридору с выключенным светом: вроде бы всё просто, но стоит оступиться — и вот вы уже споткнулись о табуретку (или получили в консоли ошибку Cannot read property 'value' of null). Причины ошибок могут быть самыми разными: опечатки, неправильный порядок загрузки скрипта, особенности коллекций, несуществующие элементы и многое другое.
В этой лекции мы разберём самые частые и коварные ошибки, которые подстерегают новичков при работе с DOM. Если вы хотя бы раз видели в консоли сообщение типа Uncaught TypeError: Cannot read properties of null, эта лекция для вас!
2. Ошибка №1: Попытка обратиться к элементу, которого ещё нет
Как это выглядит
<!DOCTYPE html>
<html>
<head>
<title>Ошибка №1</title>
<script>
// Ой! Скрипт подключён в <head>, а элемента ещё нет
const btn = document.getElementById('myButton');
btn.addEventListener('click', function() {
alert('Кнопка нажата!');
});
</script>
</head>
<body>
<button id="myButton">Нажми меня</button>
</body>
</html>
Что происходит?
Скрипт выполняется сразу при загрузке <head>, а кнопка <button id="myButton"> ещё не появилась в DOM. В результате btn оказывается равен null, и попытка вызвать addEventListener приводит к ошибке.
Как избежать
Подключайте скрипт в конце <body>:
<body>
<button id="myButton">Нажми меня</button>
<script src="script.js"></script>
</body>
Или используйте событие DOMContentLoaded:
document.addEventListener('DOMContentLoaded', function() {
const btn = document.getElementById('myButton');
btn.addEventListener('click', function() {
alert('Кнопка нажата!');
});
});
Аналогия: Представьте, что вы пришли на вечеринку раньше всех и пытаетесь поздороваться с гостями, которых ещё нет. Лучше подождать, пока все придут!
3. Ошибка №2: Опечатки в id, class или селекторе
Пример
const title = document.getElementById('titel'); // Ой, должно быть 'title'
title.textContent = 'Привет!';
Если в HTML написано <h1 id="title">, а вы ищете 'titel', то получите null. Любая попытка обратиться к свойству такого объекта вызовет ошибку.
Как избежать
- Проверяйте имена id/class в HTML и в JS — они должны совпадать до буквы!
- Используйте автодополнение IDE (например, VS Code или WebStorm) — она поможет избежать опечаток.
- Если элемент не найден, getElementById всегда вернёт null — это сигнал к перепроверке.
4. Ошибка №3: Работа с коллекциями как с массивами
Методы getElementsByClassName и getElementsByTagName возвращают HTMLCollection или NodeList, а не настоящий массив.
Пример
const items = document.getElementsByClassName('item');
items.forEach(item => {
item.textContent = 'Элемент!';
});
Ошибка: items.forEach is not a function — потому что HTMLCollection не поддерживает forEach в старых браузерах.
Как избежать
Используйте цикл for или for...of:
for (let item of items) {
item.textContent = 'Элемент!';
}
Или превратите коллекцию в массив:
Array.from(items).forEach(item => {
item.textContent = 'Элемент!';
});
Современный querySelectorAll возвращает NodeList, который поддерживает forEach:
document.querySelectorAll('.item').forEach(item => {
item.textContent = 'Элемент!';
});
Интересный факт: В новых браузерах NodeList уже поддерживает forEach, но HTMLCollection — нет.
5. Ошибка №4: Изменение коллекции во время перебора
Если вы удаляете или добавляете элементы в DOM прямо во время перебора коллекции, коллекция может "поехать" и цикл пропустит часть элементов или зациклится.
Пример
const items = document.getElementsByClassName('item');
for (let i = 0; i < items.length; i++) {
items[i].remove(); // Опа! Коллекция укорачивается прямо во время перебора
}
Чем это плохо? При удалении элемента коллекция становится короче, и некоторые элементы могут быть пропущены.
Как избежать
Перебирать коллекцию с конца:
for (let i = items.length - 1; i >= 0; i--) {
items[i].remove();
}
Или сначала собрать массив ссылок на элементы, а потом удалять:
const itemsArr = Array.from(items);
itemsArr.forEach(item => item.remove());
6. Ошибка №5: Попытка изменить несуществующий элемент
Если поиск по DOM ничего не дал, а вы сразу начинаете что-то менять — получите ошибку.
Пример
const subtitle = document.getElementById('subtitle');
subtitle.textContent = 'Сюрприз!'; // subtitle === null, будет ошибка
Как избежать
Проверяйте элемент на null:
if (subtitle) {
subtitle.textContent = 'Сюрприз!';
}
Или используйте оператор опциональной цепочки:
subtitle?.textContent = 'Сюрприз!';
7. Ошибка №6: Не тот результат поиска — коллекция вместо одного элемента
Методы getElementsByClassName и querySelectorAll возвращают коллекцию, даже если на странице один элемент. Попытка сразу обратиться к свойству приведёт к ошибке.
Пример
const items = document.getElementsByClassName('header');
items.textContent = 'Заголовок'; // Ошибка! items — коллекция
Как избежать
Обращайтесь к элементу по индексу:
items[0].textContent = 'Заголовок';
Или используйте querySelector, чтобы получить первый подходящий элемент:
const header = document.querySelector('.header');
header.textContent = 'Заголовок';
8. Ошибка №7: Множественное изменение DOM — "Рисуем по одному пикселю"
Если вы часто меняете DOM в цикле (например, добавляете 100 элементов по одному), страница может заметно "подтормаживать".
Пример
for (let i = 0; i < 100; i++) {
const li = document.createElement('li');
li.textContent = 'Элемент ' + i;
document.body.appendChild(li); // Каждый раз перерисовка!
}
Как избежать
Используйте фрагменты:
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
const li = document.createElement('li');
li.textContent = 'Элемент ' + i;
fragment.appendChild(li);
}
document.body.appendChild(fragment); // Одна операция вставки!
Аналогия: Гораздо быстрее принести 100 писем в офис одной пачкой, чем бегать по одному.
9. Ошибка №8: Перезапись innerHTML вместо изменения textContent
Иногда хочется поменять только текст внутри элемента, а случайно перезаписывается всё содержимое, включая вложенные теги.
Пример
const box = document.getElementById('box');
box.innerHTML = 'Просто текст'; // Удалит всё, включая вложенные элементы!
Как избежать
Если нужно поменять только текст — используйте textContent:
box.textContent = 'Просто текст';
Используйте innerHTML только если хотите действительно заменить всё содержимое, включая теги.
10. Ошибка №9: Ошибки при переборе коллекций через for...in
for...in перебирает все свойства объекта, включая унаследованные, а не только элементы коллекции. Это часто приводит к сюрпризам.
Пример
const items = document.getElementsByClassName('item');
for (let key in items) {
items[key].textContent = 'Ой!';
}
Как избежать
Используйте for или for...of:
for (let item of items) {
item.textContent = 'Ок!';
}
11. Ошибка №10: Множественное навешивание обработчиков событий
Если вы несколько раз навешиваете обработчик на один и тот же элемент, он будет срабатывать столько раз, сколько обработчиков.
Пример
const btn = document.getElementById('myButton');
btn.addEventListener('click', () => console.log('Клик!'));
btn.addEventListener('click', () => console.log('Клик!'));
Как избежать
- Назначайте обработчик только один раз, или снимайте старый обработчик при необходимости.
- Используйте именованные функции, чтобы можно было снять обработчик через removeEventListener.
12. Ошибка №11: Использование устаревших методов поиска элементов
Методы вроде document.all, document.layers и даже document.getElementsByTagName('*') — это привет из прошлого века. Они используются редко и могут работать не так, как вы ожидаете.
Как избежать
Используйте современные методы: getElementById, querySelector, querySelectorAll.
13. Ошибка №12: Неправильная работа с коллекциями "живыми" и "статичными"
getElementsByClassName и getElementsByTagName возвращают живые коллекции — они автоматически обновляются при изменении DOM. А вот querySelectorAll возвращает статическую коллекцию — она не меняется после поиска.
Пример
const items = document.getElementsByClassName('item');
const staticItems = document.querySelectorAll('.item');
document.body.innerHTML += '<div class="item">Новый</div>';
console.log(items.length); // увеличится!
console.log(staticItems.length); // останется прежним
Как избежать сюрпризов
- Если нужно работать со "снимком" коллекции — используйте querySelectorAll.
- Если нужно всегда актуальное состояние DOM — используйте "живые" коллекции, но будьте осторожны при изменениях внутри цикла.
14. Ошибка №13: Нарушение вложенности тегов
Иногда попытка добавить элемент в неподходящее место (например, <div> внутрь <ul>) приводит к странному поведению DOM.
Пример
const ul = document.querySelector('ul');
const div = document.createElement('div');
ul.appendChild(div); // Некорректно: внутри <ul> должны быть только <li>
Как избежать
Следите за правильной структурой HTML — используйте <li> внутри <ul>, <tr> внутри <table>, и так далее.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ