В Airbnb при клике на карточку жилья нужно найти ближайший контейнер с данными объявления, прочитать его ID и открыть нужную страницу. В Notion при нажатии на блок нужно найти его родительскую страницу. DOM-навигация — это умение перемещаться по дереву HTML-элементов из JavaScript.
Когда у тебя есть ссылка на один элемент, нужно уметь найти его соседей, родителей или детей без повторного вызова querySelector. Это быстрее и позволяет писать универсальные обработчики.
document
└── html
├── head
│ └── title
└── body
├── header
│ └── nav
└── main
├── section.products
│ ├── article.card
│ └── article.card
└── asideelement.parentElement // родительский элемент (null для html)
element.parentNode // родительский узел (включая document)element.children // HTMLCollection — только элементы (без текстовых узлов)
element.childNodes // NodeList — все узлы включая текст и комментарии
element.firstElementChild // первый дочерний элемент
element.lastElementChild // последний дочерний элементРазница между children и childNodes: пробелы и переносы строки между тегами становятся текстовыми узлами — они попадают в childNodes, но не в children.
element.nextElementSibling // следующий сосед-элемент
element.previousElementSibling // предыдущий сосед-элемент
element.nextSibling // следующий узел (может быть текстом)Метод closest(selector) идёт вверх по дереву и возвращает первого предка (включая сам элемент), соответствующего селектору. Незаменим в делегировании событий:
// Клик по любому элементу внутри карточки товара -> найти саму карточку
productList.addEventListener('click', (event) => {
const card = event.target.closest('.product-card')
if (!card) return // клик был вне карточки
const productId = card.dataset.id
openProduct(productId)
})element.matches(selector) — возвращает true, если элемент соответствует CSS-селектору:
if (element.matches('.active')) { /* элемент активен */ }
if (element.matches('li:first-child')) { /* первый элемент списка */ }Код выполняется в sandbox без реального DOM. Примеры ниже симулируют DOM-структуру через обычные объекты. В реальном браузере свойства children, parentElement и другие работают точно так же.
Ошибка 1: children vs childNodes — текстовые узлы
// childNodes[0] может быть текстовым узлом пробела или переноса строки!
// children[0] — всегда первый дочерний элемент
// Безопасно:
const first = list.firstElementChild // первый элемент-потомок
const count = list.children.length // количество дочерних элементовОшибка 2: closest() не находит элемент — неверный селектор
// Неправильно — ищем 'card' но класс 'product-card'
const bad = e.target.closest('product-card') // null, нет такого тега
// Правильно — с точкой для класса
const good = e.target.closest('.product-card') // находитОшибка 3: nextSibling вместо nextElementSibling
const li = list.firstElementChild
li.nextSibling // может быть текстовый узел!
li.nextElementSibling // всегда следующий элемент-соседevent.target.closest('.card') для карточек товаровСимуляция DOM-дерева через объекты и навигация: родители, дети, соседи, closest
// Симуляция DOM-структуры как вложенных объектов
function createNode(tag, attrs, children) {
if (attrs === undefined) attrs = {}
if (children === undefined) children = []
const node = {
tag,
attrs,
children,
parent: null,
get firstElementChild() { return this.children[0] ?? null },
get lastElementChild() { return this.children[this.children.length - 1] ?? null },
get nextElementSibling() {
if (!this.parent) return null
const idx = this.parent.children.indexOf(this)
return this.parent.children[idx + 1] ?? null
},
get previousElementSibling() {
if (!this.parent) return null
const idx = this.parent.children.indexOf(this)
return this.parent.children[idx - 1] ?? null
},
closest(selector) {
let current = this
while (current) {
const matchesTag = current.tag === selector
const cls = (current.attrs.class || '').split(' ')
const matchesClass = selector.startsWith('.') && cls.includes(selector.slice(1))
if (matchesTag || matchesClass) return current
current = current.parent
}
return null
}
}
children.forEach(function(child) { child.parent = node })
return node
}
// Структура каталога товаров
const btn1 = createNode('button', { class: 'buy-btn' })
const img1 = createNode('img')
const card1 = createNode('article', { class: 'product-card', id: '1' }, [img1, btn1])
const btn2 = createNode('button', { class: 'buy-btn' })
const card2 = createNode('article', { class: 'product-card', id: '2' }, [btn2])
const grid = createNode('section', { class: 'products' }, [card1, card2])
const main = createNode('main', {}, [grid])
// Навигация
console.log(grid.firstElementChild.attrs.id) // '1'
console.log(grid.lastElementChild.attrs.id) // '2'
console.log(card1.nextElementSibling.attrs.id) // '2'
console.log(card2.previousElementSibling.attrs.id) // '1'
console.log(card1.parent.tag) // 'section'
// closest — от кнопки к карточке (делегирование событий)
const foundCard = btn1.closest('.product-card')
console.log(foundCard.attrs.id) // '1'
// Поиск предка по тегу
console.log(btn1.closest('main').tag) // 'main'
console.log(btn1.closest('aside')) // null — нет такого предкаВ Airbnb при клике на карточку жилья нужно найти ближайший контейнер с данными объявления, прочитать его ID и открыть нужную страницу. В Notion при нажатии на блок нужно найти его родительскую страницу. DOM-навигация — это умение перемещаться по дереву HTML-элементов из JavaScript.
Когда у тебя есть ссылка на один элемент, нужно уметь найти его соседей, родителей или детей без повторного вызова querySelector. Это быстрее и позволяет писать универсальные обработчики.
document
└── html
├── head
│ └── title
└── body
├── header
│ └── nav
└── main
├── section.products
│ ├── article.card
│ └── article.card
└── asideelement.parentElement // родительский элемент (null для html)
element.parentNode // родительский узел (включая document)element.children // HTMLCollection — только элементы (без текстовых узлов)
element.childNodes // NodeList — все узлы включая текст и комментарии
element.firstElementChild // первый дочерний элемент
element.lastElementChild // последний дочерний элементРазница между children и childNodes: пробелы и переносы строки между тегами становятся текстовыми узлами — они попадают в childNodes, но не в children.
element.nextElementSibling // следующий сосед-элемент
element.previousElementSibling // предыдущий сосед-элемент
element.nextSibling // следующий узел (может быть текстом)Метод closest(selector) идёт вверх по дереву и возвращает первого предка (включая сам элемент), соответствующего селектору. Незаменим в делегировании событий:
// Клик по любому элементу внутри карточки товара -> найти саму карточку
productList.addEventListener('click', (event) => {
const card = event.target.closest('.product-card')
if (!card) return // клик был вне карточки
const productId = card.dataset.id
openProduct(productId)
})element.matches(selector) — возвращает true, если элемент соответствует CSS-селектору:
if (element.matches('.active')) { /* элемент активен */ }
if (element.matches('li:first-child')) { /* первый элемент списка */ }Код выполняется в sandbox без реального DOM. Примеры ниже симулируют DOM-структуру через обычные объекты. В реальном браузере свойства children, parentElement и другие работают точно так же.
Ошибка 1: children vs childNodes — текстовые узлы
// childNodes[0] может быть текстовым узлом пробела или переноса строки!
// children[0] — всегда первый дочерний элемент
// Безопасно:
const first = list.firstElementChild // первый элемент-потомок
const count = list.children.length // количество дочерних элементовОшибка 2: closest() не находит элемент — неверный селектор
// Неправильно — ищем 'card' но класс 'product-card'
const bad = e.target.closest('product-card') // null, нет такого тега
// Правильно — с точкой для класса
const good = e.target.closest('.product-card') // находитОшибка 3: nextSibling вместо nextElementSibling
const li = list.firstElementChild
li.nextSibling // может быть текстовый узел!
li.nextElementSibling // всегда следующий элемент-соседevent.target.closest('.card') для карточек товаровСимуляция DOM-дерева через объекты и навигация: родители, дети, соседи, closest
// Симуляция DOM-структуры как вложенных объектов
function createNode(tag, attrs, children) {
if (attrs === undefined) attrs = {}
if (children === undefined) children = []
const node = {
tag,
attrs,
children,
parent: null,
get firstElementChild() { return this.children[0] ?? null },
get lastElementChild() { return this.children[this.children.length - 1] ?? null },
get nextElementSibling() {
if (!this.parent) return null
const idx = this.parent.children.indexOf(this)
return this.parent.children[idx + 1] ?? null
},
get previousElementSibling() {
if (!this.parent) return null
const idx = this.parent.children.indexOf(this)
return this.parent.children[idx - 1] ?? null
},
closest(selector) {
let current = this
while (current) {
const matchesTag = current.tag === selector
const cls = (current.attrs.class || '').split(' ')
const matchesClass = selector.startsWith('.') && cls.includes(selector.slice(1))
if (matchesTag || matchesClass) return current
current = current.parent
}
return null
}
}
children.forEach(function(child) { child.parent = node })
return node
}
// Структура каталога товаров
const btn1 = createNode('button', { class: 'buy-btn' })
const img1 = createNode('img')
const card1 = createNode('article', { class: 'product-card', id: '1' }, [img1, btn1])
const btn2 = createNode('button', { class: 'buy-btn' })
const card2 = createNode('article', { class: 'product-card', id: '2' }, [btn2])
const grid = createNode('section', { class: 'products' }, [card1, card2])
const main = createNode('main', {}, [grid])
// Навигация
console.log(grid.firstElementChild.attrs.id) // '1'
console.log(grid.lastElementChild.attrs.id) // '2'
console.log(card1.nextElementSibling.attrs.id) // '2'
console.log(card2.previousElementSibling.attrs.id) // '1'
console.log(card1.parent.tag) // 'section'
// closest — от кнопки к карточке (делегирование событий)
const foundCard = btn1.closest('.product-card')
console.log(foundCard.attrs.id) // '1'
// Поиск предка по тегу
console.log(btn1.closest('main').tag) // 'main'
console.log(btn1.closest('aside')) // null — нет такого предкаИспользуй вспомогательную функцию createNode и напиши три функции для навигации по DOM-дереву: getDepth(node) — глубина узла от корня (корень = 0); getPath(node) — массив тегов от корня до узла; getSiblings(node) — массив всех соседних узлов (без самого узла).
getDepth: while (node.parent) { depth++; node = node.parent }. getPath: let cur = node; while (cur) { path.push(cur.tag); cur = cur.parent } затем path.reverse(). getSiblings: node.parent.children.filter(child => child !== node).