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

DOM-дерево

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

Ты открываешь Notion и создаёшь новый блок текста. Страница не перезагружается — текст появляется мгновенно. Как это работает? JavaScript изменяет страницу прямо в браузере, не обращаясь к серверу.

DOM (Document Object Model) — это JavaScript-представление HTML-страницы в виде дерева объектов. Каждый HTML-тег становится узлом дерева, которым можно управлять из JS.

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

  • «Объекты» — элементы DOM это объекты со свойствами и методами
  • «Массивы» — querySelectorAll возвращает NodeList, похожий на массив
  • «?.» — optional chaining незаменим при работе с DOM
  • Поиск элементов

    // Один элемент — первое совпадение по CSS-селектору
    const btn = document.querySelector('.submit-btn')
    const form = document.querySelector('#login-form')
    const h1 = document.querySelector('h1')
    
    // Все совпадения — NodeList (forEach есть, map — нет!)
    const cards = document.querySelectorAll('.product-card')
    cards.forEach(card => card.classList.add('loaded'))
    
    // Для методов массива — конвертируй:
    const prices = Array.from(document.querySelectorAll('.price'))
      .map(el => parseFloat(el.textContent))

    Чтение и изменение содержимого

    const el = document.querySelector('.product-title')
    
    // textContent — безопасно, обрабатывает как текст
    el.textContent = 'MacBook Pro 16"'
    
    // innerHTML — опасно с пользовательскими данными! XSS-уязвимость
    el.innerHTML = '<b>MacBook Pro</b>'       // можно, если данные не от пользователя
    el.innerHTML = '<b>' + userName + '</b>'  // НЕЛЬЗЯ — XSS!
    
    // value — для полей ввода
    const input = document.querySelector('#search')
    console.log(input.value)  // текущее значение поля
    input.value = ''          // очистить поле

    Атрибуты и data-атрибуты

    const link = document.querySelector('a')
    link.getAttribute('href')           // '/products/42'
    link.setAttribute('href', '/cart')
    link.removeAttribute('disabled')
    
    // dataset — удобный доступ к data-* атрибутам
    // HTML: <button data-product-id="42" data-action="add-to-cart">
    const btn = document.querySelector('button')
    console.log(btn.dataset.productId)  // '42' (snake-case → camelCase автоматически)
    console.log(btn.dataset.action)     // 'add-to-cart'
    btn.dataset.productId = '99'        // устанавливает data-product-id="99"

    classList — управление классами

    const card = document.querySelector('.product-card')
    
    card.classList.add('in-cart')          // добавить класс
    card.classList.remove('loading')       // удалить класс
    card.classList.toggle('selected')      // добавить если нет, убрать если есть
    card.classList.contains('active')      // true/false
    
    // toggle с принудительным значением
    card.classList.toggle('sale', product.onSale)  // true = добавить, false = убрать

    Создание и добавление элементов

    function createProductCard(product) {
      const card = document.createElement('div')
      card.className = 'product-card'
      card.dataset.id = product.id
    
      const title = document.createElement('h3')
      title.textContent = product.name  // безопасно — не парсит HTML
    
      const price = document.createElement('p')
      price.textContent = product.price.toLocaleString('ru-RU') + ' ₽'
    
      card.append(title, price)  // добавляет несколько узлов сразу
      return card
    }
    
    const container = document.querySelector('#products')
    container.appendChild(createProductCard({ id: 1, name: 'Ноутбук', price: 80000 }))

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

    1. innerHTML с пользовательскими данными — XSS:

    // Сломано — XSS уязвимость:
    const comment = getUserInput()  // '<script>stealCookies()</script>'
    el.innerHTML = 'Комментарий: ' + comment  // выполнит скрипт!
    
    // Исправлено:
    el.textContent = 'Комментарий: ' + comment  // отобразит как текст

    2. querySelector вернул null — не проверили:

    // Сломано — TypeError если элемента нет на странице:
    const btn = document.querySelector('#nonexistent')
    btn.addEventListener('click', handler)  // Cannot read properties of null
    
    // Исправлено — optional chaining:
    const btn = document.querySelector('#submit-btn')
    btn?.addEventListener('click', handler)

    3. Затёрли все классы вместо добавления одного:

    // Сломано — затирает существующие классы:
    el.className = 'active'   // было 'card card--large' — всё потеряно!
    
    // Исправлено:
    el.classList.add('active')  // добавляет к существующим

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

  • React, Vue, Angular — Virtual DOM абстрагирует прямые манипуляции, но под капотом те же операции
  • Vanilla JS: Toast-уведомления, модальные окна, infinite scroll — чистый DOM
  • Web Components: кастомные HTML-элементы (<my-dropdown>) с Shadow DOM
  • Заметка о sandbox

    В этом курсе document недоступен — используем объекты-симуляторы с теми же методами.

    Примеры

    Симуляция DOM: создание карточек товаров интернет-магазина

    // Симуляция DOM API для sandbox (в браузере — то же самое, но нативно)
    function makeElement(tag) {
      return {
        tag,
        textContent: '',
        className: '',
        dataset: {},
        children: [],
        classList: {
          _cls: new Set(),
          add(...names) { names.forEach(n => this._cls.add(n)) },
          remove(n) { this._cls.delete(n) },
          toggle(n, force) {
            const has = this._cls.has(n)
            if (force !== undefined) { force ? this._cls.add(n) : this._cls.delete(n) }
            else { has ? this._cls.delete(n) : this._cls.add(n) }
          },
          contains(n) { return this._cls.has(n) },
          toString() { return [...this._cls].join(' ') },
        },
        append(...nodes) { nodes.forEach(n => this.children.push(n)) },
        appendChild(child) { this.children.push(child); return child },
      }
    }
    const document = { createElement: makeElement }
    
    // Функция создания карточки — идентична браузерному коду
    function createProductCard(product) {
      const card = document.createElement('div')
      card.className = 'product-card'
      card.dataset.id = product.id
    
      const title = document.createElement('h3')
      title.className = 'product-card__title'
      title.textContent = product.name   // textContent — безопасно
    
      const price = document.createElement('p')
      price.className = 'product-card__price'
      price.textContent = product.price.toLocaleString('ru-RU') + ' ₽'
    
      const badge = document.createElement('span')
      badge.classList.add('badge')
      badge.classList.toggle('badge--sale', product.onSale)
      badge.textContent = product.onSale ? 'Скидка' : 'Новинка'
    
      const btn = document.createElement('button')
      btn.className = 'btn btn--primary'
      btn.dataset.productId = product.id
      btn.dataset.action = 'add-to-cart'
      btn.textContent = 'В корзину'
    
      card.append(title, price, badge, btn)
      return card
    }
    
    const products = [
      { id: 1, name: 'MacBook Pro 14"', price: 189990, onSale: true },
      { id: 2, name: 'AirPods Pro',     price: 24990,  onSale: false },
      { id: 3, name: 'Magic Mouse',     price: 8990,   onSale: true },
    ]
    
    const container = document.createElement('div')
    products.forEach(p => container.appendChild(createProductCard(p)))
    
    // Читаем структуру как HTML
    container.children.forEach(card => {
      const [t, p, b, btn] = card.children
      console.log(`<div class="${card.className}" data-id="${card.dataset.id}">`)
      console.log(`  <h3 class="${t.className}">${t.textContent}</h3>`)
      console.log(`  <p class="${p.className}">${p.textContent}</p>`)
      console.log(`  <span class="${b.classList}">${b.textContent}</span>`)
      console.log(`  <button data-action="${btn.dataset.action}">${btn.textContent}</button>`)
      console.log('</div>')
    })

    DOM-дерево

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

    Ты открываешь Notion и создаёшь новый блок текста. Страница не перезагружается — текст появляется мгновенно. Как это работает? JavaScript изменяет страницу прямо в браузере, не обращаясь к серверу.

    DOM (Document Object Model) — это JavaScript-представление HTML-страницы в виде дерева объектов. Каждый HTML-тег становится узлом дерева, которым можно управлять из JS.

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

  • «Объекты» — элементы DOM это объекты со свойствами и методами
  • «Массивы» — querySelectorAll возвращает NodeList, похожий на массив
  • «?.» — optional chaining незаменим при работе с DOM
  • Поиск элементов

    // Один элемент — первое совпадение по CSS-селектору
    const btn = document.querySelector('.submit-btn')
    const form = document.querySelector('#login-form')
    const h1 = document.querySelector('h1')
    
    // Все совпадения — NodeList (forEach есть, map — нет!)
    const cards = document.querySelectorAll('.product-card')
    cards.forEach(card => card.classList.add('loaded'))
    
    // Для методов массива — конвертируй:
    const prices = Array.from(document.querySelectorAll('.price'))
      .map(el => parseFloat(el.textContent))

    Чтение и изменение содержимого

    const el = document.querySelector('.product-title')
    
    // textContent — безопасно, обрабатывает как текст
    el.textContent = 'MacBook Pro 16"'
    
    // innerHTML — опасно с пользовательскими данными! XSS-уязвимость
    el.innerHTML = '<b>MacBook Pro</b>'       // можно, если данные не от пользователя
    el.innerHTML = '<b>' + userName + '</b>'  // НЕЛЬЗЯ — XSS!
    
    // value — для полей ввода
    const input = document.querySelector('#search')
    console.log(input.value)  // текущее значение поля
    input.value = ''          // очистить поле

    Атрибуты и data-атрибуты

    const link = document.querySelector('a')
    link.getAttribute('href')           // '/products/42'
    link.setAttribute('href', '/cart')
    link.removeAttribute('disabled')
    
    // dataset — удобный доступ к data-* атрибутам
    // HTML: <button data-product-id="42" data-action="add-to-cart">
    const btn = document.querySelector('button')
    console.log(btn.dataset.productId)  // '42' (snake-case → camelCase автоматически)
    console.log(btn.dataset.action)     // 'add-to-cart'
    btn.dataset.productId = '99'        // устанавливает data-product-id="99"

    classList — управление классами

    const card = document.querySelector('.product-card')
    
    card.classList.add('in-cart')          // добавить класс
    card.classList.remove('loading')       // удалить класс
    card.classList.toggle('selected')      // добавить если нет, убрать если есть
    card.classList.contains('active')      // true/false
    
    // toggle с принудительным значением
    card.classList.toggle('sale', product.onSale)  // true = добавить, false = убрать

    Создание и добавление элементов

    function createProductCard(product) {
      const card = document.createElement('div')
      card.className = 'product-card'
      card.dataset.id = product.id
    
      const title = document.createElement('h3')
      title.textContent = product.name  // безопасно — не парсит HTML
    
      const price = document.createElement('p')
      price.textContent = product.price.toLocaleString('ru-RU') + ' ₽'
    
      card.append(title, price)  // добавляет несколько узлов сразу
      return card
    }
    
    const container = document.querySelector('#products')
    container.appendChild(createProductCard({ id: 1, name: 'Ноутбук', price: 80000 }))

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

    1. innerHTML с пользовательскими данными — XSS:

    // Сломано — XSS уязвимость:
    const comment = getUserInput()  // '<script>stealCookies()</script>'
    el.innerHTML = 'Комментарий: ' + comment  // выполнит скрипт!
    
    // Исправлено:
    el.textContent = 'Комментарий: ' + comment  // отобразит как текст

    2. querySelector вернул null — не проверили:

    // Сломано — TypeError если элемента нет на странице:
    const btn = document.querySelector('#nonexistent')
    btn.addEventListener('click', handler)  // Cannot read properties of null
    
    // Исправлено — optional chaining:
    const btn = document.querySelector('#submit-btn')
    btn?.addEventListener('click', handler)

    3. Затёрли все классы вместо добавления одного:

    // Сломано — затирает существующие классы:
    el.className = 'active'   // было 'card card--large' — всё потеряно!
    
    // Исправлено:
    el.classList.add('active')  // добавляет к существующим

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

  • React, Vue, Angular — Virtual DOM абстрагирует прямые манипуляции, но под капотом те же операции
  • Vanilla JS: Toast-уведомления, модальные окна, infinite scroll — чистый DOM
  • Web Components: кастомные HTML-элементы (<my-dropdown>) с Shadow DOM
  • Заметка о sandbox

    В этом курсе document недоступен — используем объекты-симуляторы с теми же методами.

    Примеры

    Симуляция DOM: создание карточек товаров интернет-магазина

    // Симуляция DOM API для sandbox (в браузере — то же самое, но нативно)
    function makeElement(tag) {
      return {
        tag,
        textContent: '',
        className: '',
        dataset: {},
        children: [],
        classList: {
          _cls: new Set(),
          add(...names) { names.forEach(n => this._cls.add(n)) },
          remove(n) { this._cls.delete(n) },
          toggle(n, force) {
            const has = this._cls.has(n)
            if (force !== undefined) { force ? this._cls.add(n) : this._cls.delete(n) }
            else { has ? this._cls.delete(n) : this._cls.add(n) }
          },
          contains(n) { return this._cls.has(n) },
          toString() { return [...this._cls].join(' ') },
        },
        append(...nodes) { nodes.forEach(n => this.children.push(n)) },
        appendChild(child) { this.children.push(child); return child },
      }
    }
    const document = { createElement: makeElement }
    
    // Функция создания карточки — идентична браузерному коду
    function createProductCard(product) {
      const card = document.createElement('div')
      card.className = 'product-card'
      card.dataset.id = product.id
    
      const title = document.createElement('h3')
      title.className = 'product-card__title'
      title.textContent = product.name   // textContent — безопасно
    
      const price = document.createElement('p')
      price.className = 'product-card__price'
      price.textContent = product.price.toLocaleString('ru-RU') + ' ₽'
    
      const badge = document.createElement('span')
      badge.classList.add('badge')
      badge.classList.toggle('badge--sale', product.onSale)
      badge.textContent = product.onSale ? 'Скидка' : 'Новинка'
    
      const btn = document.createElement('button')
      btn.className = 'btn btn--primary'
      btn.dataset.productId = product.id
      btn.dataset.action = 'add-to-cart'
      btn.textContent = 'В корзину'
    
      card.append(title, price, badge, btn)
      return card
    }
    
    const products = [
      { id: 1, name: 'MacBook Pro 14"', price: 189990, onSale: true },
      { id: 2, name: 'AirPods Pro',     price: 24990,  onSale: false },
      { id: 3, name: 'Magic Mouse',     price: 8990,   onSale: true },
    ]
    
    const container = document.createElement('div')
    products.forEach(p => container.appendChild(createProductCard(p)))
    
    // Читаем структуру как HTML
    container.children.forEach(card => {
      const [t, p, b, btn] = card.children
      console.log(`<div class="${card.className}" data-id="${card.dataset.id}">`)
      console.log(`  <h3 class="${t.className}">${t.textContent}</h3>`)
      console.log(`  <p class="${p.className}">${p.textContent}</p>`)
      console.log(`  <span class="${b.classList}">${b.textContent}</span>`)
      console.log(`  <button data-action="${btn.dataset.action}">${btn.textContent}</button>`)
      console.log('</div>')
    })

    Задание

    Ты разрабатываешь страницу корзины для интернет-магазина. Используя симулятор DOM (уже подготовлен), напиши функцию `renderCart(items, container)`, которая для каждого товара создаёт `<li>` с: - классом `cart-item` - `data-id` равным `item.id` - textContent вида: `"Ноутбук — 80 000 ₽ x1"` После рендеринга выведи количество товаров и общую сумму заказа.

    Подсказка

    li.classList.add("cart-item"). li.dataset.id = item.id. li.textContent = item.name + " — " + item.price.toLocaleString("ru-RU") + " ₽ x" + item.qty. container.appendChild(li).

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