data-атрибуты и dataset
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
Кнопке нужно знать, какой товар она покупает; строке списка — какой комментарий она удаляет. Прятать это в class или в глобальный массив не нужно: в HTML есть отдельное пространство имён data-*, а у элемента — свойство dataset.
У HTML фиксированный словарь атрибутов: href, src, disabled — за каждым закреплён смысл, и браузер знает, что с ним делать. Своё product-id туда не впишешь: валидатор поругается, а завтра спецификация может занять это имя под что-то своё — и разметка внезапно начнёт что-то значить.
Поэтому в HTML выделили целое пространство имён: всё, что начинается с data-, принадлежит вам. Браузер такие атрибуты не трогает — не рисует, не меняет поведение, просто хранит. Зато их видно в DevTools, по ним работают CSS-селекторы вроде [data-state="open"], и из JavaScript к ним есть короткий вход.
dataset — не копия и не снимок. Это живая обёртка (объект типа DOMStringMap) над всеми data--атрибутами элемента: читаете свойство — читаете атрибут, пишете свойство — переписываете атрибут. В обе стороны и без синхронизации руками.
Отсутствующий ключ даёт undefined — не пустую строку и не null, который вернул бы getAttribute. Новое свойство добавляет атрибут в конец списка, а delete убирает его из разметки целиком.
В разметке имена принято писать через дефис, а в JavaScript дефис в имени свойства не наберёшь. Браузер переводит одно в другое по простому правилу: приставка data- отбрасывается, а дефис вместе со следующей строчной буквой превращается в заглавную. Обратный путь такой же: заглавная буква в свойстве становится дефисом и строчной буквой в атрибуте.
| В разметке | В JavaScript | Что важно |
|---|---|---|
| data-id="7" | el.dataset.id | значение — строка "7", не число |
| data-user-id="7" | el.dataset.userId | дефис со строчной буквой схлопывается |
| data-order-42="ok" | el.dataset["order-42"] | перед цифрой дефис остаётся |
| атрибута нет | el.dataset.nope | undefined, а не пустая строка |
| data-page-u-r-l="/home" | el.dataset.pageURL = "/home" | обратный путь: заглавная → дефис |
Дефис исчезает, только если сразу за ним идёт строчная латинская буква. Перед цифрой он остаётся, и ключ приходится брать в скобках. А обратный перевод ничего не знает про аббревиатуры: каждая заглавная буква станет отдельным дефисом, и pageURL превратится в data-page-u-r-l.
Самый частый приём: положить в разметку ключ строки — id товара, комментария, пользователя. Тогда обработчику не нужны ни параллельные массивы, ни разбор текста: элемент носит свой идентификатор с собой. Особенно это удобно с делегированием, когда слушатель один на весь список.
Одна деталь: event.target — это самый глубокий элемент под курсором, например span внутри пункта. У него своего data-id нет, поэтому от цели поднимаются вверх через closest до ближайшего элемента с нужным атрибутом.
Если вы пишете class="user-42", остановитесь: это data-атрибут, который стесняется своего имени.
В атрибуте живёт только текст. Число придётся пропустить через Number, флаг — сравнить со строкой, объект — сложить через JSON.stringify и разобрать через JSON.parse. Последнее уместно для маленькой конфигурации в пару полей; всё, что крупнее, в разметке только мешает.
И помните, где эти данные лежат: в открытой разметке, которую видит любой посетитель через DevTools и просмотр исходного кода. Токены, чужие адреса и цены, которым нельзя доверять на клиенте, туда не кладут. Зато data-state отлично заменяет служебные классы: селектор [data-state="open"] в CSS и одна строчка в JavaScript.
Если объект нужен только коду и никогда — стилям, разметке и другим разработчикам, храните его рядом в Map или WeakMap с элементом в роли ключа. Разметка остаётся чистой, значения не превращаются в строки, а WeakMap отпустит запись, когда на элемент не останется сильных ссылок и его заберёт сборщик мусора — удаление из документа само по себе ещё ничего не освобождает.
- Атрибуты, начинающиеся с data-, официально отданы разработчику: браузер их хранит, но никак не интерпретирует.
- dataset — живая двусторонняя обёртка над этими атрибутами: запись в свойство меняет разметку, изменение атрибута видно в свойстве.
- Имена переводятся автоматически: data-user-id ↔ dataset.userId; перед цифрой дефис остаётся, а заглавные буквы при записи разворачиваются в дефисы.
- Значения всегда строки: число получают через Number, объект — через JSON.stringify и JSON.parse. Отсутствующий ключ даёт undefined.
- Классический приём — хранить id прямо в элементе и доставать его в делегировании через event.target.closest("[data-id]"); для тяжёлых объектов лучше WeakMap.
1/3В разметке есть <div data-user-id="7">. Что вернёт el.dataset.userId?
2/3Выполнили el.dataset.pageURL = "/home". Какой атрибут появится в разметке?
3/3Слушатель висит на списке, у пунктов есть data-id, а внутри пункта — span с текстом. Кликнули по span. Что вернёт event.target.dataset.id?
Сумма корзины из разметки
В каждом пункте списка лежат цена и количество. Посчитайте итог, читая их через dataset. Не забудьте, что оба значения — строки.
Перебираем пункты и приводим оба значения к числу — иначе + склеит строки.
Короче — через reduce: [...items].reduce((sum, item) => sum + Number(item.dataset.price) * Number(item.dataset.qty), 0).
Переключатель состояния
У кнопки есть data-state со значением off. Повесьте обработчик клика, который переключает состояние между off и on. Три клика подряд должны оставить кнопку включённой.
Читаем текущее значение из dataset и записываем противоположное — атрибут в разметке обновится сам.
Дальше внешний вид можно задать одним правилом CSS вместо возни с классами: селектор [data-state="on"] сработает сразу после присваивания.