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

Атрибуты и свойства DOM

Ты хочешь прочитать начальное значение value из поля ввода, но input.value возвращает то, что пользователь уже ввёл, а не то, что стояло в HTML. Или добавляешь data-product-id в разметку, а потом не знаешь, как достать это в JS. Всё это — разница между HTML-атрибутами и DOM-свойствами.

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

  • «DOM» — DOM-объекты как представление HTML-элементов
  • «querySelector» — поиск элементов для работы с их атрибутами
  • «Proxy/Reflect» — dataset в примере реализован через Proxy
  • HTML-атрибуты vs DOM-свойства

    <input id="email" type="email" value="admin@site.ru" class="field">

    | HTML-атрибут | DOM-свойство | Тип |

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

    | value="admin@site.ru" | input.value | string |

    | class="field" | input.className | string |

    | disabled | input.disabled | boolean |

    | href="/page" | a.href | абсолютный URL |

    getAttribute / setAttribute

    Работают именно с HTML-атрибутами (строки, как в разметке):

    const link = document.querySelector('a.nav-link')
    
    link.getAttribute('href')           // '/products'  (исходная строка)
    link.href                           // 'https://site.ru/products'  (DOM-свойство, абсолютный URL)
    
    link.setAttribute('href', '/cart')  // меняет HTML-атрибут
    link.getAttribute('href')           // '/cart'
    
    link.removeAttribute('disabled')    // убирает атрибут
    link.hasAttribute('disabled')       // false

    Синхронизация атрибутов и свойств

    Изменение атрибута обновляет свойство, но не всегда наоборот:

    const input = document.querySelector('#email')
    // Начальное состояние: атрибут value = "admin@site.ru"
    
    // Пользователь вводит новый текст...
    // input.value теперь 'new@mail.ru' (свойство изменилось)
    // input.getAttribute('value') всё ещё 'admin@site.ru' (атрибут не менялся)
    
    // setAttribute обновляет и атрибут, и свойство:
    input.setAttribute('value', 'reset@mail.ru')
    input.getAttribute('value')  // 'reset@mail.ru'
    input.value                  // 'reset@mail.ru'

    data-* атрибуты и dataset API

    Стандартный способ хранить произвольные данные в HTML:

    <div class="product-card"
         data-product-id="1042"
         data-category="electronics"
         data-in-stock="true">
      ...
    </div>
    const card = document.querySelector('.product-card')
    
    // Чтение через dataset (camelCase автоматически)
    card.dataset.productId    // '1042'   (data-product-id → productId)
    card.dataset.category     // 'electronics'
    card.dataset.inStock      // 'true'  (значения — всегда строки)
    
    // Запись
    card.dataset.productId = '2000'  // меняет data-product-id="2000" в HTML
    
    // Удаление
    delete card.dataset.inStock      // убирает атрибут data-in-stock

    Правило преобразования: data-user-profile-id ↔ dataset.userProfileId (kebab-case → camelCase).

    aria-* для доступности

    const btn = document.querySelector('.menu-toggle')
    
    btn.setAttribute('aria-expanded', 'true')
    btn.setAttribute('aria-label', 'Закрыть меню')
    btn.getAttribute('aria-expanded')   // 'true'

    Нестандартные атрибуты

    Можно использовать любые атрибуты через getAttribute/setAttribute, но для пользовательских данных рекомендуются data-*:

    // Не рекомендуется — может конфликтовать с будущими стандартами
    element.setAttribute('myattr', 'value')
    
    // Правильно — используй data-*
    element.dataset.myAttr = 'value'

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

    1. input.value vs getAttribute('value') — читают разное:

    // После того как пользователь изменил поле:
    // getAttribute('value') → начальное значение из HTML
    // input.value → текущее значение (то, что ввёл пользователь)
    
    // Правило: для работы с текущим значением поля — используй .value
    // Для сброса на начальное — используй setAttribute

    2. dataset значения всегда строки — не забудь конвертировать:

    // HTML: <div data-count="5" data-active="true">
    const div = document.querySelector('div')
    
    const count = div.dataset.count
    console.log(count + 1)         // '51' — строка + число = конкатенация!
    console.log(Number(count) + 1) // 6 — правильно
    
    const active = div.dataset.active
    console.log(active === true)            // false — строка !== boolean
    console.log(active === 'true')          // true — правильное сравнение

    3. setAttribute всегда принимает строку — boolean атрибуты работают иначе:

    // HTML boolean атрибуты: disabled, checked, required
    // Их наличие = true, отсутствие = false
    
    // Плохо: устанавливает строку "false", но элемент всё равно disabled!
    input.setAttribute('disabled', 'false')  // disabled="false" — элемент ВСЁ ЕЩЁ disabled
    
    // Хорошо:
    input.removeAttribute('disabled')  // убрать atribute = включить элемент
    input.disabled = false              // через DOM-свойство

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

  • **data-* атрибуты** — хранение ID, категорий, состояний прямо в HTML для event delegation
  • **aria-* атрибуты** — доступность: aria-expanded, aria-label, aria-hidden меняются динамически
  • href vs getAttribute('href') — href даёт абсолютный URL, getAttribute — исходную строку
  • Инициализация JS-компонентов — querySelectorAll('[data-component]') + dataset.component
  • Примечание о sandbox

    Следующие примеры используют mock-объект, имитирующий поведение DOM-элемента, чтобы показать принципы работы атрибутов и свойств без реального браузера.

    Примеры

    Mock-элемент с getAttribute/setAttribute и автоматической синхронизацией dataset

    // Mock DOM-элемент с полной поддержкой атрибутов и dataset
    function createMockElement(tag, initialAttrs = {}) {
      const attrMap = new Map(Object.entries(initialAttrs))
    
      // dataset — прокси: camelCase ↔ kebab-case
      const dataset = new Proxy({}, {
        get(_, prop) {
          // userId → data-user-id
          const key = 'data-' + prop.replace(/([A-Z])/g, '-$1').toLowerCase()
          return attrMap.get(key)
        },
        set(_, prop, value) {
          const key = 'data-' + prop.replace(/([A-Z])/g, '-$1').toLowerCase()
          attrMap.set(key, String(value))
          return true
        },
        deleteProperty(_, prop) {
          const key = 'data-' + prop.replace(/([A-Z])/g, '-$1').toLowerCase()
          attrMap.delete(key)
          return true
        },
      })
    
      return {
        tag,
        dataset,
        getAttribute(name) {
          return attrMap.get(name) ?? null
        },
        setAttribute(name, value) {
          attrMap.set(name, String(value))
        },
        removeAttribute(name) {
          attrMap.delete(name)
        },
        hasAttribute(name) {
          return attrMap.has(name)
        },
        get className() { return attrMap.get('class') ?? '' },
        set className(v) { attrMap.set('class', v) },
      }
    }
    
    // Карточка товара
    const card = createMockElement('div', {
      'class': 'product-card',
      'data-product-id': '1042',
      'data-category': 'electronics',
      'data-in-stock': 'true',
    })
    
    // getAttribute
    console.log(card.getAttribute('class'))          // 'product-card'
    console.log(card.getAttribute('data-product-id')) // '1042'
    console.log(card.getAttribute('nonexistent'))     // null
    
    // dataset
    console.log(card.dataset.productId)   // '1042'  (kebab → camel)
    console.log(card.dataset.category)    // 'electronics'
    console.log(card.dataset.inStock)     // 'true'
    
    // Изменение через dataset отображается в getAttribute
    card.dataset.productId = '2000'
    console.log(card.getAttribute('data-product-id')) // '2000'
    
    // setAttribute
    card.setAttribute('aria-expanded', 'false')
    console.log(card.hasAttribute('aria-expanded'))   // true
    card.removeAttribute('aria-expanded')
    console.log(card.hasAttribute('aria-expanded'))   // false
    
    // className
    card.className = 'product-card featured'
    console.log(card.getAttribute('class'))           // 'product-card featured'

    Атрибуты и свойства DOM

    Ты хочешь прочитать начальное значение value из поля ввода, но input.value возвращает то, что пользователь уже ввёл, а не то, что стояло в HTML. Или добавляешь data-product-id в разметку, а потом не знаешь, как достать это в JS. Всё это — разница между HTML-атрибутами и DOM-свойствами.

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

  • «DOM» — DOM-объекты как представление HTML-элементов
  • «querySelector» — поиск элементов для работы с их атрибутами
  • «Proxy/Reflect» — dataset в примере реализован через Proxy
  • HTML-атрибуты vs DOM-свойства

    <input id="email" type="email" value="admin@site.ru" class="field">

    | HTML-атрибут | DOM-свойство | Тип |

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

    | value="admin@site.ru" | input.value | string |

    | class="field" | input.className | string |

    | disabled | input.disabled | boolean |

    | href="/page" | a.href | абсолютный URL |

    getAttribute / setAttribute

    Работают именно с HTML-атрибутами (строки, как в разметке):

    const link = document.querySelector('a.nav-link')
    
    link.getAttribute('href')           // '/products'  (исходная строка)
    link.href                           // 'https://site.ru/products'  (DOM-свойство, абсолютный URL)
    
    link.setAttribute('href', '/cart')  // меняет HTML-атрибут
    link.getAttribute('href')           // '/cart'
    
    link.removeAttribute('disabled')    // убирает атрибут
    link.hasAttribute('disabled')       // false

    Синхронизация атрибутов и свойств

    Изменение атрибута обновляет свойство, но не всегда наоборот:

    const input = document.querySelector('#email')
    // Начальное состояние: атрибут value = "admin@site.ru"
    
    // Пользователь вводит новый текст...
    // input.value теперь 'new@mail.ru' (свойство изменилось)
    // input.getAttribute('value') всё ещё 'admin@site.ru' (атрибут не менялся)
    
    // setAttribute обновляет и атрибут, и свойство:
    input.setAttribute('value', 'reset@mail.ru')
    input.getAttribute('value')  // 'reset@mail.ru'
    input.value                  // 'reset@mail.ru'

    data-* атрибуты и dataset API

    Стандартный способ хранить произвольные данные в HTML:

    <div class="product-card"
         data-product-id="1042"
         data-category="electronics"
         data-in-stock="true">
      ...
    </div>
    const card = document.querySelector('.product-card')
    
    // Чтение через dataset (camelCase автоматически)
    card.dataset.productId    // '1042'   (data-product-id → productId)
    card.dataset.category     // 'electronics'
    card.dataset.inStock      // 'true'  (значения — всегда строки)
    
    // Запись
    card.dataset.productId = '2000'  // меняет data-product-id="2000" в HTML
    
    // Удаление
    delete card.dataset.inStock      // убирает атрибут data-in-stock

    Правило преобразования: data-user-profile-id ↔ dataset.userProfileId (kebab-case → camelCase).

    aria-* для доступности

    const btn = document.querySelector('.menu-toggle')
    
    btn.setAttribute('aria-expanded', 'true')
    btn.setAttribute('aria-label', 'Закрыть меню')
    btn.getAttribute('aria-expanded')   // 'true'

    Нестандартные атрибуты

    Можно использовать любые атрибуты через getAttribute/setAttribute, но для пользовательских данных рекомендуются data-*:

    // Не рекомендуется — может конфликтовать с будущими стандартами
    element.setAttribute('myattr', 'value')
    
    // Правильно — используй data-*
    element.dataset.myAttr = 'value'

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

    1. input.value vs getAttribute('value') — читают разное:

    // После того как пользователь изменил поле:
    // getAttribute('value') → начальное значение из HTML
    // input.value → текущее значение (то, что ввёл пользователь)
    
    // Правило: для работы с текущим значением поля — используй .value
    // Для сброса на начальное — используй setAttribute

    2. dataset значения всегда строки — не забудь конвертировать:

    // HTML: <div data-count="5" data-active="true">
    const div = document.querySelector('div')
    
    const count = div.dataset.count
    console.log(count + 1)         // '51' — строка + число = конкатенация!
    console.log(Number(count) + 1) // 6 — правильно
    
    const active = div.dataset.active
    console.log(active === true)            // false — строка !== boolean
    console.log(active === 'true')          // true — правильное сравнение

    3. setAttribute всегда принимает строку — boolean атрибуты работают иначе:

    // HTML boolean атрибуты: disabled, checked, required
    // Их наличие = true, отсутствие = false
    
    // Плохо: устанавливает строку "false", но элемент всё равно disabled!
    input.setAttribute('disabled', 'false')  // disabled="false" — элемент ВСЁ ЕЩЁ disabled
    
    // Хорошо:
    input.removeAttribute('disabled')  // убрать atribute = включить элемент
    input.disabled = false              // через DOM-свойство

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

  • **data-* атрибуты** — хранение ID, категорий, состояний прямо в HTML для event delegation
  • **aria-* атрибуты** — доступность: aria-expanded, aria-label, aria-hidden меняются динамически
  • href vs getAttribute('href') — href даёт абсолютный URL, getAttribute — исходную строку
  • Инициализация JS-компонентов — querySelectorAll('[data-component]') + dataset.component
  • Примечание о sandbox

    Следующие примеры используют mock-объект, имитирующий поведение DOM-элемента, чтобы показать принципы работы атрибутов и свойств без реального браузера.

    Примеры

    Mock-элемент с getAttribute/setAttribute и автоматической синхронизацией dataset

    // Mock DOM-элемент с полной поддержкой атрибутов и dataset
    function createMockElement(tag, initialAttrs = {}) {
      const attrMap = new Map(Object.entries(initialAttrs))
    
      // dataset — прокси: camelCase ↔ kebab-case
      const dataset = new Proxy({}, {
        get(_, prop) {
          // userId → data-user-id
          const key = 'data-' + prop.replace(/([A-Z])/g, '-$1').toLowerCase()
          return attrMap.get(key)
        },
        set(_, prop, value) {
          const key = 'data-' + prop.replace(/([A-Z])/g, '-$1').toLowerCase()
          attrMap.set(key, String(value))
          return true
        },
        deleteProperty(_, prop) {
          const key = 'data-' + prop.replace(/([A-Z])/g, '-$1').toLowerCase()
          attrMap.delete(key)
          return true
        },
      })
    
      return {
        tag,
        dataset,
        getAttribute(name) {
          return attrMap.get(name) ?? null
        },
        setAttribute(name, value) {
          attrMap.set(name, String(value))
        },
        removeAttribute(name) {
          attrMap.delete(name)
        },
        hasAttribute(name) {
          return attrMap.has(name)
        },
        get className() { return attrMap.get('class') ?? '' },
        set className(v) { attrMap.set('class', v) },
      }
    }
    
    // Карточка товара
    const card = createMockElement('div', {
      'class': 'product-card',
      'data-product-id': '1042',
      'data-category': 'electronics',
      'data-in-stock': 'true',
    })
    
    // getAttribute
    console.log(card.getAttribute('class'))          // 'product-card'
    console.log(card.getAttribute('data-product-id')) // '1042'
    console.log(card.getAttribute('nonexistent'))     // null
    
    // dataset
    console.log(card.dataset.productId)   // '1042'  (kebab → camel)
    console.log(card.dataset.category)    // 'electronics'
    console.log(card.dataset.inStock)     // 'true'
    
    // Изменение через dataset отображается в getAttribute
    card.dataset.productId = '2000'
    console.log(card.getAttribute('data-product-id')) // '2000'
    
    // setAttribute
    card.setAttribute('aria-expanded', 'false')
    console.log(card.hasAttribute('aria-expanded'))   // true
    card.removeAttribute('aria-expanded')
    console.log(card.hasAttribute('aria-expanded'))   // false
    
    // className
    card.className = 'product-card featured'
    console.log(card.getAttribute('class'))           // 'product-card featured'

    Задание

    Реализуй класс MockElement с поддержкой: getAttribute(name), setAttribute(name, value), removeAttribute(name), hasAttribute(name) — хранящий атрибуты в Map. А также свойство dataset, которое автоматически преобразует camelCase в kebab-case для ключей data-* атрибутов (чтение и запись).

    Подсказка

    Для camelCase → kebab-case используй: "data-" + prop.replace(/([A-Z])/g, "-$1").toLowerCase(). getAttribute возвращает this._attrs.get(name) ?? null. hasAttribute — this._attrs.has(name).

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