Ты хочешь прочитать начальное значение value из поля ввода, но input.value возвращает то, что пользователь уже ввёл, а не то, что стояло в HTML. Или добавляешь data-product-id в разметку, а потом не знаешь, как достать это в JS. Всё это — разница между HTML-атрибутами и DOM-свойствами.
dataset в примере реализован через Proxy<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 |
Работают именно с 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'Стандартный способ хранить произвольные данные в 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).
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
// Для сброса на начальное — используй setAttribute2. 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-свойствоhref даёт абсолютный URL, getAttribute — исходную строкуquerySelectorAll('[data-component]') + dataset.componentСледующие примеры используют 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'Ты хочешь прочитать начальное значение value из поля ввода, но input.value возвращает то, что пользователь уже ввёл, а не то, что стояло в HTML. Или добавляешь data-product-id в разметку, а потом не знаешь, как достать это в JS. Всё это — разница между HTML-атрибутами и DOM-свойствами.
dataset в примере реализован через Proxy<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 |
Работают именно с 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'Стандартный способ хранить произвольные данные в 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).
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
// Для сброса на начальное — используй setAttribute2. 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-свойствоhref даёт абсолютный URL, getAttribute — исходную строкуquerySelectorAll('[data-component]') + dataset.componentСледующие примеры используют 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).