← JavaScript/Поиск элементов: querySelector и getElement*#121 из 383← ПредыдущийСледующий →+20 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: JS базаПрактика: async и сетьТермин: Closure

Поиск элементов: querySelector и getElement*

Ты делаешь корзину товаров: по клику на кнопку "Добавить" нужно найти ближайшую карточку, достать из неё цену и id, обновить счётчик в шапке. Всё это начинается с поиска нужных элементов в DOM. Неправильный метод поиска — и ты получаешь "живую" коллекцию, которая меняется прямо во время итерации.

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

  • «DOM» — что такое DOM, дерево элементов, узлы
  • «DOM навигация» — parentNode, children, nextSibling
  • «Массивы» — Array.from(), forEach, spread для конвертации коллекций
  • getElementById

    Самый быстрый способ найти элемент по уникальному идентификатору:

    const modal = document.getElementById('checkout-modal')
    const form  = document.getElementById('login-form')
    // Возвращает Element | null

    getElementsByClassName и getElementsByTagName

    Возвращают живую HTMLCollection — она автоматически обновляется при изменении DOM:

    const cards    = document.getElementsByClassName('product-card')
    const inputs   = document.getElementsByTagName('input')
    
    console.log(cards.length)  // 5
    // если добавить новую карточку в DOM → cards.length сразу станет 6

    Живая коллекция может вызывать неожиданное поведение в циклах — лучше конвертировать в массив:

    const arr = Array.from(document.getElementsByClassName('item'))

    querySelector — первый по CSS-селектору

    Принимает любой CSS-селектор и возвращает первый подходящий элемент или null:

    document.querySelector('.product-card')          // первая карточка
    document.querySelector('#nav > a.active')        // активная ссылка в #nav
    document.querySelector('[data-role="submit"]')   // по data-атрибуту
    document.querySelector('form input[required]')   // первый required input в форме

    querySelectorAll — все по CSS-селектору

    Возвращает статичный NodeList — снимок DOM на момент вызова:

    const allCards   = document.querySelectorAll('.product-card')
    const allLinks   = document.querySelectorAll('nav a')
    const checkedBoxes = document.querySelectorAll('input[type="checkbox"]:checked')
    
    // NodeList — итерируемый, поддерживает forEach и for...of
    allCards.forEach(card => card.classList.add('loaded'))
    
    // Но spread нужен для методов массива:
    const hrefs = [...allLinks].map(a => a.getAttribute('href'))

    Живая vs статичная коллекция

    | Метод | Тип | Обновляется при изменении DOM |

    |---|---|---|

    | getElementsByClassName | HTMLCollection | Да (живая) |

    | getElementsByTagName | HTMLCollection | Да (живая) |

    | querySelectorAll | NodeList | Нет (статичная) |

    Поиск внутри элемента

    Любой элемент также имеет методы querySelector и querySelectorAll — поиск ведётся только среди его потомков:

    const sidebar = document.querySelector('.sidebar')
    const sidebarLinks = sidebar.querySelectorAll('a')       // только ссылки внутри sidebar
    const firstBtn     = sidebar.querySelector('button')     // первая кнопка в sidebar

    Реальные сценарии

    // Карточка товара по data-id
    const card = document.querySelector('[data-product-id="42"]')
    
    // Первый невалидный input
    const invalid = document.querySelector('input:invalid')
    
    // Кнопки с data-action
    const actionBtns = document.querySelectorAll('[data-action]')
    
    // Элемент с aria-expanded="true"
    const expanded = document.querySelector('[aria-expanded="true"]')

    Типичные ошибки

    1. Итерация живой HTMLCollection с удалением элементов:

    const items = document.getElementsByClassName('item')
    
    // Плохо: живая коллекция меняется при удалении
    for (let i = 0; i < items.length; i++) {
      items[i].remove()  // после удаления items.length уменьшается — пропускаем элементы!
    }
    
    // Хорошо: конвертируй в статичный массив сначала
    Array.from(items).forEach(item => item.remove())

    2. Забыть проверить на null перед использованием:

    // Плохо: querySelector вернёт null если элемент не найден
    const btn = document.querySelector('.submit-btn')
    btn.addEventListener('click', handler)  // TypeError: Cannot read properties of null
    
    // Хорошо: optional chaining
    document.querySelector('.submit-btn')?.addEventListener('click', handler)

    3. NodeList — не массив, у него нет .map() и .filter():

    const links = document.querySelectorAll('a')
    
    // Плохо:
    links.map(a => a.href)  // TypeError: links.map is not a function
    
    // Хорошо:
    Array.from(links).map(a => a.href)
    [...links].map(a => a.href)

    В реальных проектах

  • Event delegation — один обработчик на родителе, поиск цели через closest()
  • Инициализация компонентов — querySelectorAll('[data-component]') для массовой инициализации
  • Форм-валидация — form.querySelectorAll('input:invalid') после submit
  • Testing Library — screen.getByRole, screen.getByText работают по тем же принципам
  • Примечание о sandbox

    Поскольку код выполняется в изолированном окружении без настоящего DOM, примеры ниже симулируют поведение браузерных методов через JavaScript-объекты. Принципы работы — те же самые, что и в браузере.

    Примеры

    Симуляция querySelectorAll через обход дерева mock-объектов

    // Симулируем узлы DOM как обычные объекты
    function el(tag, attrs = {}, children = []) {
      return { tag, attrs, children }
    }
    
    // Дерево: ul > [li.item, li.item.active, li.item, div.promo]
    const root = el('ul', { id: 'menu' }, [
      el('li', { class: 'item' }, [
        el('a', { href: '/home', 'data-section': 'home' }),
      ]),
      el('li', { class: 'item active' }, [
        el('a', { href: '/catalog', 'data-section': 'catalog' }),
      ]),
      el('li', { class: 'item' }, [
        el('a', { href: '/cart', 'data-section': 'cart' }),
      ]),
      el('div', { class: 'promo' }),
    ])
    
    // Рекурсивный обход — аналог querySelectorAll по тегу
    function queryAllByTag(node, tag, results = []) {
      if (node.tag === tag) results.push(node)
      node.children.forEach(child => queryAllByTag(child, tag, results))
      return results
    }
    
    // Рекурсивный обход — аналог querySelectorAll по классу
    function queryAllByClass(node, cls, results = []) {
      const classes = (node.attrs.class || '').split(' ')
      if (classes.includes(cls)) results.push(node)
      node.children.forEach(child => queryAllByClass(child, cls, results))
      return results
    }
    
    // querySelector — первый совпавший
    function querySelector(node, tag) {
      const all = queryAllByTag(node, tag)
      return all[0] ?? null
    }
    
    const allLinks = queryAllByTag(root, 'a')
    console.log('Все ссылки:', allLinks.length)            // 3
    console.log('href первой:', allLinks[0].attrs.href)    // '/home'
    
    const activeItems = queryAllByClass(root, 'active')
    console.log('Активных элементов:', activeItems.length) // 1
    console.log('Тег активного:', activeItems[0].tag)      // 'li'
    
    const firstLink = querySelector(root, 'a')
    console.log('Первая ссылка ведёт на:', firstLink?.attrs.href) // '/home'
    
    // Поиск по data-атрибуту
    function queryByDataAttr(node, attr, value, results = []) {
      if (node.attrs[attr] === value) results.push(node)
      node.children.forEach(child => queryByDataAttr(child, attr, value, results))
      return results
    }
    
    const catalogLink = queryByDataAttr(root, 'data-section', 'catalog')
    console.log('Ссылка каталога:', catalogLink[0]?.attrs.href) // '/catalog'

    Поиск элементов: querySelector и getElement*

    Ты делаешь корзину товаров: по клику на кнопку "Добавить" нужно найти ближайшую карточку, достать из неё цену и id, обновить счётчик в шапке. Всё это начинается с поиска нужных элементов в DOM. Неправильный метод поиска — и ты получаешь "живую" коллекцию, которая меняется прямо во время итерации.

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

  • «DOM» — что такое DOM, дерево элементов, узлы
  • «DOM навигация» — parentNode, children, nextSibling
  • «Массивы» — Array.from(), forEach, spread для конвертации коллекций
  • getElementById

    Самый быстрый способ найти элемент по уникальному идентификатору:

    const modal = document.getElementById('checkout-modal')
    const form  = document.getElementById('login-form')
    // Возвращает Element | null

    getElementsByClassName и getElementsByTagName

    Возвращают живую HTMLCollection — она автоматически обновляется при изменении DOM:

    const cards    = document.getElementsByClassName('product-card')
    const inputs   = document.getElementsByTagName('input')
    
    console.log(cards.length)  // 5
    // если добавить новую карточку в DOM → cards.length сразу станет 6

    Живая коллекция может вызывать неожиданное поведение в циклах — лучше конвертировать в массив:

    const arr = Array.from(document.getElementsByClassName('item'))

    querySelector — первый по CSS-селектору

    Принимает любой CSS-селектор и возвращает первый подходящий элемент или null:

    document.querySelector('.product-card')          // первая карточка
    document.querySelector('#nav > a.active')        // активная ссылка в #nav
    document.querySelector('[data-role="submit"]')   // по data-атрибуту
    document.querySelector('form input[required]')   // первый required input в форме

    querySelectorAll — все по CSS-селектору

    Возвращает статичный NodeList — снимок DOM на момент вызова:

    const allCards   = document.querySelectorAll('.product-card')
    const allLinks   = document.querySelectorAll('nav a')
    const checkedBoxes = document.querySelectorAll('input[type="checkbox"]:checked')
    
    // NodeList — итерируемый, поддерживает forEach и for...of
    allCards.forEach(card => card.classList.add('loaded'))
    
    // Но spread нужен для методов массива:
    const hrefs = [...allLinks].map(a => a.getAttribute('href'))

    Живая vs статичная коллекция

    | Метод | Тип | Обновляется при изменении DOM |

    |---|---|---|

    | getElementsByClassName | HTMLCollection | Да (живая) |

    | getElementsByTagName | HTMLCollection | Да (живая) |

    | querySelectorAll | NodeList | Нет (статичная) |

    Поиск внутри элемента

    Любой элемент также имеет методы querySelector и querySelectorAll — поиск ведётся только среди его потомков:

    const sidebar = document.querySelector('.sidebar')
    const sidebarLinks = sidebar.querySelectorAll('a')       // только ссылки внутри sidebar
    const firstBtn     = sidebar.querySelector('button')     // первая кнопка в sidebar

    Реальные сценарии

    // Карточка товара по data-id
    const card = document.querySelector('[data-product-id="42"]')
    
    // Первый невалидный input
    const invalid = document.querySelector('input:invalid')
    
    // Кнопки с data-action
    const actionBtns = document.querySelectorAll('[data-action]')
    
    // Элемент с aria-expanded="true"
    const expanded = document.querySelector('[aria-expanded="true"]')

    Типичные ошибки

    1. Итерация живой HTMLCollection с удалением элементов:

    const items = document.getElementsByClassName('item')
    
    // Плохо: живая коллекция меняется при удалении
    for (let i = 0; i < items.length; i++) {
      items[i].remove()  // после удаления items.length уменьшается — пропускаем элементы!
    }
    
    // Хорошо: конвертируй в статичный массив сначала
    Array.from(items).forEach(item => item.remove())

    2. Забыть проверить на null перед использованием:

    // Плохо: querySelector вернёт null если элемент не найден
    const btn = document.querySelector('.submit-btn')
    btn.addEventListener('click', handler)  // TypeError: Cannot read properties of null
    
    // Хорошо: optional chaining
    document.querySelector('.submit-btn')?.addEventListener('click', handler)

    3. NodeList — не массив, у него нет .map() и .filter():

    const links = document.querySelectorAll('a')
    
    // Плохо:
    links.map(a => a.href)  // TypeError: links.map is not a function
    
    // Хорошо:
    Array.from(links).map(a => a.href)
    [...links].map(a => a.href)

    В реальных проектах

  • Event delegation — один обработчик на родителе, поиск цели через closest()
  • Инициализация компонентов — querySelectorAll('[data-component]') для массовой инициализации
  • Форм-валидация — form.querySelectorAll('input:invalid') после submit
  • Testing Library — screen.getByRole, screen.getByText работают по тем же принципам
  • Примечание о sandbox

    Поскольку код выполняется в изолированном окружении без настоящего DOM, примеры ниже симулируют поведение браузерных методов через JavaScript-объекты. Принципы работы — те же самые, что и в браузере.

    Примеры

    Симуляция querySelectorAll через обход дерева mock-объектов

    // Симулируем узлы DOM как обычные объекты
    function el(tag, attrs = {}, children = []) {
      return { tag, attrs, children }
    }
    
    // Дерево: ul > [li.item, li.item.active, li.item, div.promo]
    const root = el('ul', { id: 'menu' }, [
      el('li', { class: 'item' }, [
        el('a', { href: '/home', 'data-section': 'home' }),
      ]),
      el('li', { class: 'item active' }, [
        el('a', { href: '/catalog', 'data-section': 'catalog' }),
      ]),
      el('li', { class: 'item' }, [
        el('a', { href: '/cart', 'data-section': 'cart' }),
      ]),
      el('div', { class: 'promo' }),
    ])
    
    // Рекурсивный обход — аналог querySelectorAll по тегу
    function queryAllByTag(node, tag, results = []) {
      if (node.tag === tag) results.push(node)
      node.children.forEach(child => queryAllByTag(child, tag, results))
      return results
    }
    
    // Рекурсивный обход — аналог querySelectorAll по классу
    function queryAllByClass(node, cls, results = []) {
      const classes = (node.attrs.class || '').split(' ')
      if (classes.includes(cls)) results.push(node)
      node.children.forEach(child => queryAllByClass(child, cls, results))
      return results
    }
    
    // querySelector — первый совпавший
    function querySelector(node, tag) {
      const all = queryAllByTag(node, tag)
      return all[0] ?? null
    }
    
    const allLinks = queryAllByTag(root, 'a')
    console.log('Все ссылки:', allLinks.length)            // 3
    console.log('href первой:', allLinks[0].attrs.href)    // '/home'
    
    const activeItems = queryAllByClass(root, 'active')
    console.log('Активных элементов:', activeItems.length) // 1
    console.log('Тег активного:', activeItems[0].tag)      // 'li'
    
    const firstLink = querySelector(root, 'a')
    console.log('Первая ссылка ведёт на:', firstLink?.attrs.href) // '/home'
    
    // Поиск по data-атрибуту
    function queryByDataAttr(node, attr, value, results = []) {
      if (node.attrs[attr] === value) results.push(node)
      node.children.forEach(child => queryByDataAttr(child, attr, value, results))
      return results
    }
    
    const catalogLink = queryByDataAttr(root, 'data-section', 'catalog')
    console.log('Ссылка каталога:', catalogLink[0]?.attrs.href) // '/catalog'

    Задание

    В тестовом фреймворке нужна функция findAll(mockDom, selector). Она принимает корневой узел mock-дерева и строку selector в формате "tag" (например "li"), ".class" (например ".active") или "[attr=value]" (например "[data-id=2]"). Функция должна вернуть массив всех узлов, соответствующих селектору.

    Подсказка

    selector.slice(1) даст имя класса без точки. Для атрибута: selector.slice(1, -1) уберёт скобки, затем split("=") разделит на ключ и значение. Не забудь рекурсию по node.children.

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