JavaRush /Курсы /Модуль 1: Web Core /Разбор типичных ошибок при работе с DOM-деревом

Разбор типичных ошибок при работе с DOM-деревом

Модуль 1: Web Core
22 уровень , 4 лекция
Открыта

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>, и так далее.

1
Задача
Модуль 1: Web Core, 22 уровень, 4 лекция
Недоступна
Получение данных Axios
Получение данных Axios
1
Задача
Модуль 1: Web Core, 22 уровень, 4 лекция
Недоступна
Отправка данных Fetch
Отправка данных Fetch
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ