Що таке DOM?
DOM (Document Object Model) - це живе дерево об'єктів, яке браузер будує з HTML-документа в пам'яті, дозволяючи JavaScript читати, змінювати, додавати і видаляти вузли без перезавантаження сторінки.
Теорія
TL;DR
- DOM - це те, що браузер збудував з HTML і тримає в пам'яті як змінюване дерево об'єктів, а не статичний файл
- Аналогія: HTML - роздрукований рецепт; DOM - жива кухня, де шеф-кухар може поміняти або додати інгредієнти прямо під час готування
- Головна різниця: HTML парситься один раз і лежить на диску; DOM живе в пам'яті й постійно змінюється
- Прямі DOM API - для дрібних скриптів і розширень; React або Preact - коли оновлення стають частими або складними
Швидкий приклад
<!DOCTYPE html>
<html>
<body>
<p id="demo">Оригінальний текст</p>
<script>
const para = document.getElementById('demo');
para.textContent = 'DOM змінив це!'; // Без перезавантаження
console.log(para.nodeName); // "P"
</script>
</body>
</html>getElementById повертає живий об'єкт вузла. Зміна textContent одразу запускає перемальовування в рендерному пайплайні браузера. Це і є весь цикл.
HTML проти DOM
HTML - статичний файл розмітки. Браузер парсить його один раз і будує дерево DOM в пам'яті. Після цього вихідний файл більше не потрібен: те, що бачить користувач, - це DOM. Тому JavaScript може змінювати сторінку, не торкаючись вихідного файлу.
DOM дає стандартизовані об'єкти вузлів: ElementNode, TextNode, CommentNode. У кожного є методи на кшталт appendChild(). Коли ти їх викликаєш, браузер автоматично ставить оновлення екрана в чергу.
Типи вузлів
Кожен елемент у дереві DOM - це вузол (node). Чотири типи трапляються найчастіше:
- Element node (
<div>,<p>тощо) - nodeType: 1 - Text node (текстовий вміст всередині тегів) - nodeType: 3
- Comment node (HTML-коментарі) - nodeType: 8
- Document node (корінь дерева,
document) - nodeType: 9
Це важливо при переборі. childNodes повертає всі типи, включно з текстовими вузлами від пробілів між тегами. children повертає тільки вузли-елементи. Переплутаєш - отримаєш Cannot set properties of undefined на вузлі з пробілом.
Коли використовувати
- Валідація форми або перемикання класу - прямі DOM API підходять
- Браузерні розширення або буклети - фреймворк просто недоступний
- Швидке прототипування - без налаштування збірки
- Часті або масові оновлення, наприклад рендер 1000 елементів списку - перейди на бібліотеку з virtual DOM, як React; її reconciler батчить зміни і мінімізує reflow
Що відбувається в браузері при мутації DOM
Движок Blink у Chrome зберігає DOM як C++ об'єкти Node, доступні для JavaScript через WebIDL-прив'язки. Виклик querySelector() змушує V8 обходити дерево в глибину (O(n) у гіршому випадку). Мутація на кшталт appendChild() позначає відповідну гілку як брудну і ставить у чергу перерахунок стилів, layout (reflow), paint і compositing. Якщо групувати мутації всередині requestAnimationFrame, всі ці проходи зливаються в один - так отримують 60fps.
Node.js не має DOM. Для тестування парсингу HTML у Node використовується jsdom.
Типові помилки
Помилка 1: innerHTML з даними користувача
// Неправильно: парсить і виконує розмітку
element.innerHTML = userInput; // <script>alert('XSS')</script> запускається
// Правильно: все трактується як звичайний текст
element.textContent = userInput;На код-рев'ю я бачу innerHTML = userInput набагато частіше, ніж хотілося б. Зазвичай це дедлайн-код, де людина просто хотіла щоб спрацювало, не подумавши про джерело даних. Для будь-яких даних від користувача використовуй textContent або createTextNode().
Помилка 2: Не перевіряєш null
// Неправильно
const el = document.getElementById('nonexistent'); // null
el.textContent = 'hi'; // TypeError: Cannot set property
// Правильно
if (el) el.textContent = 'hi';getElementById повертає null, якщо нічого не знайдено. Один непровірений результат ламає весь скрипт.
Помилка 3: Перебір живої колекції з мутацією
// Неправильно: getElementsByClassName повертає живу HTMLCollection
const items = document.getElementsByClassName('old-item');
Array.prototype.forEach.call(items, item => item.remove()); // пропускає кожен другий
// Правильно: спочатку перетвори на статичний масив
Array.from(document.getElementsByClassName('old-item')).forEach(
item => item.remove()
);getElementsByClassName і getElementsByTagName повертають живі колекції. Видалення елемента зміщує індекси, і цикл пропускає наступний сусід щоразу. querySelectorAll повертає статичний знімок і цієї проблеми не має.
Помилка 4: children проти childNodes
// Працює: children містить тільки Element-вузли
parent.children[0].style.color = 'red';
// Може впасти: childNodes[0] може бути TextNode з пробілом
parent.childNodes[0].style.color = 'red'; // Cannot set properties of undefinedПомилка 5: Не видаляєш обробники перед видаленням вузла
const btn = document.createElement('button');
btn.addEventListener('click', handler);
document.body.appendChild(btn);
btn.remove(); // обробник залишається в пам'яті
// Правильно
btn.removeEventListener('click', handler);
btn.remove();У single-page app, де елементи постійно створюються і видаляються, осиротілі обробники накопичуються у реальний витік пам'яті.
Де зустрічається на практиці
- React:
ReactDOM.render()перетворює JSX на реальні DOM-мутації черезcreateElementіappendChild - jQuery (легасі):
$(el).find()обгортаєquerySelectorAllз крос-браузерною сумісністю - Puppeteer:
page.$eval()виконує JavaScript на живому DOM для end-to-end тестів - WordPress / SSR-додатки: сервер рендерить HTML, потім клієнтський JavaScript гідратує DOM для інтерактивності
Можливі питання на співбесіді
Q: Яка різниця між textContent і innerHTML?
A: textContent встановлює звичайний текст і екранує все. innerHTML парсить рядок як розмітку і може виконати скрипти. Для будь-яких даних від користувача завжди використовуй textContent.
Q: Як querySelector обходить дерево?
A: Обхід у глибину, pre-order, починаючи від кореня документа. Зупиняється на першому збігу. Тому він швидший за querySelectorAll, якщо потрібен один елемент.
Q: Яка різниця між реальним DOM і virtual DOM?
A: Реальний DOM - фактичне дерево браузера. Virtual DOM - звичайний JavaScript-об'єкт, що його відображає. React порівнює старе і нове віртуальне дерево, потім застосовує мінімальний набір змін до реального DOM.
Q: Які є типи вузлів у DOM?
A: Основні: Element (nodeType 1), Text (nodeType 3), Comment (nodeType 8), Document (nodeType 9). Перевіряєш через node.nodeType.
Q (рівень senior): Що відбувається в Blink, коли викликаєш appendChild()?
A: Вузол вставляється в C++ дерево. Це позначає відповідну гілку як брудну і планує прохід перерахунку стилів, потім layout (reflow для геометрії), paint (малювання пікселів) і compositing. Якщо групувати мутації до наступного кадру через requestAnimationFrame, всі ці проходи зливаються в один - так отримуєш 60fps у DOM-важкому коді.
Приклади
Базовий: зміна текстового вмісту
<!DOCTYPE html>
<html>
<body>
<p id="demo">Оригінальний текст</p>
<script>
const para = document.getElementById('demo');
para.textContent = 'Оновлено через JS'; // запускає перемальовування
console.log(para.nodeName); // "P"
</script>
</body>
</html>Знайти вузол за ID і оновити його текст - найпоширеніша операція з DOM. Без перезавантаження, без фреймворку.
Середній: живий список задач
<!DOCTYPE html>
<html>
<body>
<ul id="todos"></ul>
<input id="task" placeholder="Назва задачі" />
<button id="add">Додати</button>
<script>
const ul = document.getElementById('todos');
const input = document.getElementById('task');
document.getElementById('add').addEventListener('click', () => {
const li = document.createElement('li');
li.textContent = input.value; // textContent захищає від XSS
ul.appendChild(li);
input.value = '';
});
</script>
</body>
</html>Це те, що робить TodoMVC у своїй vanilla JS реалізації. createElement будує вузол, appendChild вставляє його, textContent тримає все безпечним. React зі своїм useState і JSX робить те саме під капотом, тільки з кроком diff перед тим як торкнутися реального DOM.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.