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

изменение≈ 7 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

У элемента страницы две памяти: разметка, из которой он вырос, и объект, которым он стал. Обычно они говорят одно и то же — а там, где расходятся, водятся самые обидные баги форм.

Разделы темы:·

Браузер читает HTML и на каждый тег создаёт объект. Атрибуты при этом никуда не деваются — они остаются частью документа, — но вдобавок для каждого стандартного атрибута браузер заводит на объекте одноимённое свойство. Получается две ручки от одной двери: getAttribute("title") и elem.title.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Посмотрите на последнюю пару. В разметке hidden — это пустая строка, в объекте — честное true. Атрибут хранит текст, свойство хранит значение нужного типа, и разбирать текст обратно в булево за вас уже кто-то сделал.

ПризнакАтрибутСвойство
Где живётВ HTML-разметкеВ объекте элемента
Тип значенияВсегда строкаСтрока, число, булево, объект
Регистр имениНе важен: TITLE и title — одно и то жеВажен: title и Title — разные
ДоступgetAttribute, setAttributeelem.title
Своё имяРазрешено любоеСвойство само не появится

Блок из примера прячет не сам атрибут hidden, а правило [hidden] { display: none; } из стилей браузера. Любое ваше правило с display для этого элемента его перебьёт: элемент, которому класс задал display: flex, останется на экране, хотя box.hidden честно скажет true. Эта ловушка с hidden разобрана в прологе.

Со стороны разметки у элемента ровно четыре инструмента, и работают они с любым атрибутом — стандартным, чужим, выдуманным. getAttribute(name) вернёт строку или null, если атрибута нет. setAttribute(name, value) запишет значение, приведя его к строке. removeAttribute(name) уберёт атрибут целиком. hasAttribute(name) ответит true или false. Имена в HTML нечувствительны к регистру, так что getAttribute("TITLE") спрашивает про тот же атрибут, что и getAttribute("title").

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Разница между «атрибута нет» и «атрибут есть, но пустой» здесь принципиальна: первый случай даёт null, второй — пустую строку. В условии обе величины ложны, поэтому if (elem.getAttribute("disabled")) — это ловушка. Спрашивайте про наличие тем, что про наличие и спрашивает: hasAttribute.

getAttributeNames() отдаёт имена массивом в порядке разметки, а attributes — коллекцию объектов-атрибутов, к которым можно обращаться и по имени.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Для стандартных атрибутов связь двусторонняя: поменяли атрибут — поменялось свойство, поменяли свойство — переписался атрибут. Это называют отражением. А вот собственное свойство, которое вы навесили на элемент, — обычное свойство JS-объекта: разметка о нём не узнает никогда.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Иногда имена по разные стороны не совпадают, и виноват тут сам JavaScript. class и for — зарезервированные слова, поэтому свойства зовутся className и htmlFor. Атрибуты при этом остаются прежними, и getAttribute("for") работает как ни в чём не бывало.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Есть места, где отражения нет специально, и это главный источник недоумения. У <input> атрибут value означает «значение по умолчанию», а свойство value — «что в поле прямо сейчас». Пользователь печатает, свойство меняется, атрибут остаётся прежним. Иначе кнопка «сбросить» не знала бы, к чему возвращаться.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Ровно тот же приём с флажками: атрибут checked задаёт начальное состояние и дублируется свойством defaultChecked, а свойство checked описывает текущее. Снять галочку кодом — не то же самое, что убрать атрибут из разметки.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Правило на каждый день: нужно то, что в поле прямо сейчас, — читайте свойство; нужно то, что было написано в разметке, — getAttribute или defaultValue.

У disabled одно состояние на двоих, поэтому свойство и атрибут ходят парой. У флажка состояний два — начальное и текущее, — и атрибут закреплён за начальным, а свойство за текущим.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

В разметку можно написать любой атрибут: браузер его сохранит и отдаст через getAttribute. Свойством он не станет — elem.order так и останется undefined, потому что такого стандартного свойства нет. Настоящая опасность в другом: сегодня имя свободно, а завтра его займёт спецификация, и ваш смысл столкнётся с браузерным.

Поэтому под свои данные отведён префикс data-. Такие имена стандарт обещает не трогать никогда, а браузер собирает их в объект dataset. Перевод имён, границы приёма и приёмы на каждый день разбирает следующая статья; здесь хватит того, что запись работает в обе стороны, а значения всегда строки.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Приятный бонус: атрибуты видны и в селекторах. document.querySelector("[data-user-id]") найдёт карточку так же уверенно, как поиск по классу, и то же самое умеет CSS.

  • Атрибут — строка в разметке, свойство — типизированное значение в объекте: имена атрибутов регистронезависимы, имена свойств — нет.
  • Разметку обслуживают четыре метода: getAttribute (null, если атрибута нет), setAttribute, removeAttribute, hasAttribute.
  • Для стандартных имён работает отражение в обе стороны; class и for отражаются под именами className и htmlFor, свои свойства не отражаются вовсе.
  • У полей формы value и checked живут отдельно от атрибутов: атрибут — начальное состояние (defaultValue, defaultChecked), свойство — текущее.
  • Свои данные держите в data-*: имя не столкнётся со стандартом, а читать и писать удобно через dataset в camelCase.

1/3Поле создано разметкой с value="Аня", затем код выполнил field.value = "Борис". Что вернёт field.getAttribute("value")?

2/3Что вернёт getAttribute для атрибута, которого у элемента нет?

3/3Элементу присвоили div.order = 5. Что покажет div.getAttribute("order")?

Вернуть поле к значению из разметки

В поле было значение из разметки, потом его перезаписали. Напишите resetToMarkup(input), которая возвращает поле к тому, что написано в HTML — не зная заранее, что именно там написано.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Значение из разметки лежит в атрибуте:

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Оператор ?? нужен на случай, когда атрибута нет: getAttribute вернёт null, а в поле попадёт строка «null». Есть вариант короче и безопаснее — браузер уже держит это же значение в свойстве defaultValue, и если атрибута не было, там пустая строка:

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Собрать все data-атрибуты в объект

Напишите collect(el), которая возвращает объект из всех атрибутов с префиксом data-. Ключи — имена без префикса, значения — строки. Остальные атрибуты в объект попадать не должны.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Имена всех атрибутов даёт getAttributeNames(), значения — getAttribute:

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

А вообще браузер проделал эту работу заранее: { ...el.dataset } даёт такой же объект, только ключи будут в camelCase — data-user-id превратится в userId, а не в user-id.