← JavaScript/Навигация по DOM#104 из 383← ПредыдущийСледующий →+25 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: JS базаПрактика: async и сетьТермин: Closure

Навигация по DOM

В Airbnb при клике на карточку жилья нужно найти ближайший контейнер с данными объявления, прочитать его ID и открыть нужную страницу. В Notion при нажатии на блок нужно найти его родительскую страницу. DOM-навигация — это умение перемещаться по дереву HTML-элементов из JavaScript.

Какую проблему решает

Когда у тебя есть ссылка на один элемент, нужно уметь найти его соседей, родителей или детей без повторного вызова querySelector. Это быстрее и позволяет писать универсальные обработчики.

На основе предыдущих уроков

  • DOM — что такое DOM, querySelector
  • События — event.target и обработчики
  • Объекты — объекты и вложенные структуры
  • Структура DOM-дерева

    document
    └── html
        ├── head
        │   └── title
        └── body
            ├── header
            │   └── nav
            └── main
                ├── section.products
                │   ├── article.card
                │   └── article.card
                └── aside

    Навигация к родителю

    element.parentElement  // родительский элемент (null для html)
    element.parentNode     // родительский узел (включая document)

    Навигация к детям

    element.children           // HTMLCollection — только элементы (без текстовых узлов)
    element.childNodes         // NodeList — все узлы включая текст и комментарии
    element.firstElementChild  // первый дочерний элемент
    element.lastElementChild   // последний дочерний элемент

    Разница между children и childNodes: пробелы и переносы строки между тегами становятся текстовыми узлами — они попадают в childNodes, но не в children.

    Навигация к соседям (siblings)

    element.nextElementSibling      // следующий сосед-элемент
    element.previousElementSibling  // предыдущий сосед-элемент
    element.nextSibling             // следующий узел (может быть текстом)

    closest() — поиск предка по селектору

    Метод closest(selector) идёт вверх по дереву и возвращает первого предка (включая сам элемент), соответствующего селектору. Незаменим в делегировании событий:

    // Клик по любому элементу внутри карточки товара -> найти саму карточку
    productList.addEventListener('click', (event) => {
      const card = event.target.closest('.product-card')
      if (!card) return  // клик был вне карточки
      const productId = card.dataset.id
      openProduct(productId)
    })

    matches() — проверка по селектору

    element.matches(selector) — возвращает true, если элемент соответствует CSS-селектору:

    if (element.matches('.active')) { /* элемент активен */ }
    if (element.matches('li:first-child')) { /* первый элемент списка */ }

    Примечание о sandbox

    Код выполняется в 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') для карточек товаров
  • Компоненты: навигация от кнопки к её форме, от пункта меню к подменю
  • Drag and Drop: определение позиции элемента среди соседей
  • Аккордеон/вкладки: скрытие/показ соседних панелей
  • Примеры

    Симуляция 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 — нет такого предка

    Навигация по DOM

    В Airbnb при клике на карточку жилья нужно найти ближайший контейнер с данными объявления, прочитать его ID и открыть нужную страницу. В Notion при нажатии на блок нужно найти его родительскую страницу. DOM-навигация — это умение перемещаться по дереву HTML-элементов из JavaScript.

    Какую проблему решает

    Когда у тебя есть ссылка на один элемент, нужно уметь найти его соседей, родителей или детей без повторного вызова querySelector. Это быстрее и позволяет писать универсальные обработчики.

    На основе предыдущих уроков

  • DOM — что такое DOM, querySelector
  • События — event.target и обработчики
  • Объекты — объекты и вложенные структуры
  • Структура DOM-дерева

    document
    └── html
        ├── head
        │   └── title
        └── body
            ├── header
            │   └── nav
            └── main
                ├── section.products
                │   ├── article.card
                │   └── article.card
                └── aside

    Навигация к родителю

    element.parentElement  // родительский элемент (null для html)
    element.parentNode     // родительский узел (включая document)

    Навигация к детям

    element.children           // HTMLCollection — только элементы (без текстовых узлов)
    element.childNodes         // NodeList — все узлы включая текст и комментарии
    element.firstElementChild  // первый дочерний элемент
    element.lastElementChild   // последний дочерний элемент

    Разница между children и childNodes: пробелы и переносы строки между тегами становятся текстовыми узлами — они попадают в childNodes, но не в children.

    Навигация к соседям (siblings)

    element.nextElementSibling      // следующий сосед-элемент
    element.previousElementSibling  // предыдущий сосед-элемент
    element.nextSibling             // следующий узел (может быть текстом)

    closest() — поиск предка по селектору

    Метод closest(selector) идёт вверх по дереву и возвращает первого предка (включая сам элемент), соответствующего селектору. Незаменим в делегировании событий:

    // Клик по любому элементу внутри карточки товара -> найти саму карточку
    productList.addEventListener('click', (event) => {
      const card = event.target.closest('.product-card')
      if (!card) return  // клик был вне карточки
      const productId = card.dataset.id
      openProduct(productId)
    })

    matches() — проверка по селектору

    element.matches(selector) — возвращает true, если элемент соответствует CSS-селектору:

    if (element.matches('.active')) { /* элемент активен */ }
    if (element.matches('li:first-child')) { /* первый элемент списка */ }

    Примечание о sandbox

    Код выполняется в 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') для карточек товаров
  • Компоненты: навигация от кнопки к её форме, от пункта меню к подменю
  • Drag and Drop: определение позиции элемента среди соседей
  • Аккордеон/вкладки: скрытие/показ соседних панелей
  • Примеры

    Симуляция 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).

    Загружаем среду выполнения...
    Загружаем AI-помощника...