Какие свойства у элементов javascript
В этой статье познакомимся DOM-свойствами и атрибутами, рассмотрим, чем они отличаются и как правильно с ними работать. Разберём какие в JavaScript есть методы для выполнения операций над атрибутами.
Чем отличается атрибут от DOM-свойства?
Атрибуты — это HTML-сущности, с помощью которых мы можем добавить определённые данные к элементам в HTML-коде.
Когда браузер запрашивает некоторую страницу, он получает её исходный HTML-код. После этого он парсит этот код и строит на его основании DOM. Во время этого процесса HTML-атрибуты элементов переводятся в соответствующие DOM-свойства.
Например, браузер, при чтении следующей HTML-строчки кода, создаст для этого элемента следующие DOM-свойства: id, className, src и alt.
<img id=»brand» class=»brand» src=»logo.png» alt=»логотип сайта»>
Обращение к этим свойствам в коде JavaScript выполняется как к свойствам объекта. Объектом здесь выступает узел (элемент) DOM.
Пример, в котором получим значения DOM-свойств для элемента, приведённого выше, и выведем их значения в консоль:
// получим элемент
var brandImg = document.querySelector(‘#brand’);
// выведем в консоль значения DOM-свойств элемента
console.log(brandImg.id); // «brand»
console.log(brandImg.className); // «brand»
console.log(brandImg.src); // «logo.png»
console.log(brandImg.alt); // «логотип сайта»
Некоторые названия DOM-свойств не соответствуют именам атрибутов. Одним из таких является атрибут class. Данному атрибуту соответствует DOM-свойство className. Данное отличие связано с тем, что class является ключевым словом в JavaScript, оно зарезервировано и не может использоваться. Из-за этого разработчики стандарта решили использовать для соответствия какое-то другое название, в качестве которого было выбрано className.
Ещё один нюанс связан с тем, что перевод HTML-атрибутов, заданных в исходном коде документа, в DOM-свойства не всегда осуществляется один к одному.
Если элемент имеет нестандартный HTML-атрибут, то свойство, соответствующее ему в DOM, не создаётся.
<div id=»mydiv» alt=»…»></div>
<script>
// получим элемент
mydiv = document.querySelector(‘#mydiv’);
// получим значение свойство alt элемента и выведем его в консоль
console.log(mydiv.alt); // undefined
// получим значение атрибут alt элемента и выведем его в консоль
console.log(mydiv.getAttribute(‘alt’)); // «…»
</script>
Другое отличие связано с тем, что значения определённых HTML-атрибутов и соответствующих им DOM-свойств могут быть различными. Т.е. атрибут может иметь одно значение, а DOM-свойство, созданное на его основе – другое.
Одним из таких атрибутов является checked.
<input type=»checkbox» checked>
Значение HTML-атрибута checked в данном случае – это пустая строка. Но, свойство, соответствующее данному атрибуту в DOM, будет иметь значение true. Т.к. по правилам стандарта для установления true достаточно лишь упоминание этого атрибута в HTML-коде и при этом не важно какое он будет иметь значение.
При этом даже если мы в HTML-коде не укажем атрибут checked для элемента input с типом checkbox, то для него в DOM всё равно будет создано свойство checked, но оно будет равно false.
<input type=»checkbox»>
Кроме этого, JavaScript позволяет также работать с атрибутами. Для этого в DOM API имеются специальные методы. Но их желательно использовать только тогда, когда вам действительно нужно работать с данными именно так.
При этом нужно знать, что, когда мы изменяем DOM-свойство элемента, изменяется и соответствующий ему атрибут, и наоборот. Но это процесс в браузерах выполнятся не всегда один к одному.
Основные отличия между DOM-свойствами и атрибутами:
- значение атрибута – это всегда строка, а значение DOM-свойства – определённый тип данных (не обязательно строка);
- имя атрибута – регистронезависимо, а DOM-свойства — регистрозависимо. Т.е. в HTML-коде мы можем, например, HTML-атрибут id написать, как Id, ID и т.д. То же касается и имени атрибута, которые мы указываем в специальных методах JavaScript для работы с ним. Но к соответствующему DOM-свойству мы можем обратиться только по id и никак по-другому.
Работа с DOM-свойствами элемента
Работа со свойствами элементов в JavaScript как уже было отмечено выше осуществляется как со свойствами объектов.
Но для того, чтобы обратиться к свойству некоторого элемента, его необходимо сначала получить. Получить DOM-элемент в JavaScript можно, например, с помощью универсального метода querySelector, а коллекцию DOM элементов, например, посредством querySelectorAll.
В качестве первого примера рассмотрим следующий HTML-элемент:
<div id=»alert» class=»alert alert-info» title=»Текст подсказки…»>
Текст информационного сообщения…
</div>
<script>
var alert = document.querySelector(‘#alert’); // получим элемент
</script>
На базе него разберём как осуществляется получение DOM-свойств, их изменение и добавление новых.
Чтение значений DOM-свойств:
// получим значение DOM свойства id
var alertId = alert.id; // «alert»
// получим значение DOM свойства className
var alertClass = alert.className; // «alert alert-info»
// получим значение DOM свойства title
var alertId = alert.title; // «Текст подсказки…»
Изменение значений DOM-свойств:
// для изменения значения DOM свойству, ему нужно просто присвоить новое значение
alert.title = «Новый текст подсказки»; // присвоим DOM-свойству title элемента новое значение
// или так (т.к. обращение к этому свойству мы уже сохранили в переменную alertId)
alertId = «Новый текст подсказки»;
// или так (т.к. обращение к этому свойству мы уже сохранили в переменную alertId)
alert.className = «alert alert-warning»;
Добавление DOM-свойств:
alert.lang = «ru»; // установим свойству lang значение равное «ru»
alert.dir = «ltr»; // установим свойство dir значение равное «ltr»
Пример, в котором выведем в консоль все значения классов, которые есть у элементов p на странице:
var paragraphs = document.querySelectorAll(«p»);
for (var i = 0, length = paragraphs.length ; i < length; i++) {
if (paragraphs[i].className) {
console.log(paragraphs[i].className);
}
Пример, в котором установим всем элементам с классом content свойство lang со значением «ru»:
var contents = document.querySelectorAll(‘.content’);
for (var i = 0, length = contents.length; i < length; i++) {
contents[i].lang = «ru»;
}
Атрибуты элементов и методы для работы с ними
Атрибуты изначально задаются в HTML-коде. Они хоть и связаны, некоторым образом, со свойствами, но это не одно и тоже. В большинстве случаев следует работать именно со свойствами, а к атрибутам обращаться только тогда, когда это действительно нужно.
Значения атрибутов, в отличие от DOM-свойств, как это уже было отмечено выше всегда является строкой.
В JavaScript для выполнения операций, связанных с атрибутами, имеется четыре метода:
- .hasAttribute(‘имя_атрибута’) – проверяет наличие указанного атрибута у элемента. Если проверяемый атрибут есть у элемента, то данный метод возвращает true, в противном случае — false.
- .getAttribute(‘имя_атрибута’) – получает значение атрибута. Если указанного атрибута нет у элемента, то данный метод возвращает пустую строку («») или null.
- .setAttribute(‘имя_атрибута’, ‘значение_атрибута’) – устанавливает указанный атрибут с указанным значением элементу. Если указанный атрибут есть у элемента, то данный метод тогда просто изменит ему значение.
- .removeAttribute(‘имя_атрибута’) — удаляет указанный атрибут у элемента.
Рассмотрим примеры.
Очень интересный пример с атрибутом value.
Пример с атрибутом value
<input name=»name» type=»text» value=»Bob»>
<script>
var name = document.querySelector(‘input[name=»name»]’); // получим элемент
</script>
Получим значение атрибута value и DOM-свойства value:
// получим значение атрибута value у элемента
name.getAttribute(‘value’); // «Bob»
// получим значение DOM-свойства value
name.value; // «Bob»
// обновим значение атрибута value, установим ему новое значение
name.setAttribute(‘value’, ‘Tom’); // «Tom»
// получим значение DOM свойства value
name.value; // «Tom»
Из этого примера видно, что, при измении атрибута value, браузер автоматически изменяет в соответствии с ним DOM-свойство value.
Теперь давайте проделаем действия, наоборот, а именно изменим значение DOM-свойства и проверим изменится ли значение атрибута:
// установим новое значение DOM-свойству value
name.value = «John»;
// получим значение атрибута value у элемента
name.getAttribute(‘value’); // «Tom»
Из этого примера видно, что не всегда изменение DOM-свойства приводит к соответствующему изменению атрибута. Т.е. в этом случае изменение DOM-свойства value не изменяет соответствующий ему атрибут.
Тоже самое произойдёт, когда пользователь будет вводить текст в это поле. В DOM-свойстве value будет находится действительное значение, а в соответствующем ему атрибуте изначальное или то, которое мы установили, например, с помощью метода setAttribute.
Этот пример показывает, что более корректно работать всегда с DOM-свойствами, а обращаться к атрибуту нужно только тогда, когда это действительно необходимо.
Даже в случае, когда вам нужно получить начальное значение value, которое мы установили в HTML, можно воспользоваться свойством. Свойство, содержащее начальное значение атрибута value называется defaultValue.
name.defaultValue; // Tom
Ещё один очень интересный пример, но теперь с атрибутом href.
Пример с атрибутом href
Пример, в котором нам нужно получить значение ссылки так, как оно было установлено в HTML.
<a id=»link» href=»page2.html»></a>
<script>
var page2 = document.querySelector(‘#link’);
page2.getAttribute(‘href’); // page2.html
page2.href; // полный URL, например: https://localhost/page2.html
</script>
В этом примере атрибут href и DOM-свойство href содержат разные значения. В атрибуте href — то, что мы установили в коде, а в DOM-свойстве — полный URL. Это различие продиктовано стандартом, в соответствии с которым браузер должен привести значение href к полному URL.
Поэтому если нам нужно получить то, что находится в атрибуте, то без метода getAttribute в этом случае не обойтись.
В завершении разберём ещё атрибут selected.
Пример с атрибутом selected
Пример, в котором показано как можно получить значение выбранной опции select:
<select id=»mark»>
<option value=»none» selected>нет оценки</option>
<option value=»1″>1</option>
<option value=»2″>2</option>
<option value=»3″>3</option>
<option value=»4″>4</option>
<option value=»5″>5</option>
</select>
<script>
// получаем элемент select
var mark = document.querySelector(‘#mark’);
// 1 способ
mark.querySelector(‘option:checked’).value;
// 2 способ
mark.value;
</script>
Пример, в котором показано как можно получить выбранные значения опций в элементе select:
<select id=»mark» multiple>
<option value=»none» selected>нет оценки</option>
<option value=»1″>1</option>
<option value=»2″>2</option>
<option value=»3″>3</option>
<option value=»4″>4</option>
<option value=»5″>5</option>
</select>
<script>
// получаем элемент select
var mark = document.querySelector(‘#mark’);
// 1 способ (через создание массива и наполнением его значений выбранных опций)
var arr = [];
for (var i = 0, length = mark.options.length; i < length; i++) {
if (mark.options[i].selected) {
arr.push(mark.options[i].value);
}
}
// 2 способ (более современный, с использованием DOM-свойства selectedOptions)
var arr = Array.from(mark.selectedOptions, option => option.value)
</script>
Ещё один способ работы с атрибутами (свойство attributes)
В JavaScript у каждого элемента имеется свойство attributes, с помощью которого можно получить все его атрибуты в виде объекта NamedNodeMap.
Данный способ может находить применение, когда вам нужно, например перебрать все атрибуты элемента.
Доступ к атрибуту в этой коллекции осуществляется по его индексу или с помощью метода item. Отсчёт атрибутов в этой коллекции ведётся с 0.
Например, выведем в консоль все атрибуты некоторого элемента:
<p id=»message» class=»text» style=»text-align: center;»>I LOVE JAVASCRIPT</p>
<script>
// получим элемент по его идентификатору message
var message = document.querySelector(‘#message’);
// получим его атрибуты
var attrs = message.attributes;
// переберём все атрибуты с помощью цикла (attrs.length – количество атрибутов)
for (var i = 0, length = attrs.length; i < length; i++) {
// attrs[i] или attrs.item(i) – обращение к атрибуту в коллекции по его порядковому номеру
// attrs[i].name – имя атрибута
// attrs[i].value – значение атрибута (с помощью него можно также изменить значение атрибута)
console.log(attrs[i].name + ‘ = ‘ + attrs[i].value);
// или с помощью метода item
console.log(attrs.item(i).name + ‘ = ‘ + attrs.item(i).value);
// пример как можно изменить значение через свойство value
// if (attrs[i].name === «class») {
// attr[i].value = «info»;
// }
}
// в результате выполнения:
// id = message
// class = text
// style = text-align: center;
</script>
Кроме этого, работать с этой коллекцией можно также посредством следующих методов:
- .getNamedItem(‘имя_aтpибyтa’) – получает значение указанного атрибута (если указанный атрибут отсутствует у элемента, то в качестве результата получим null).
- .setNamedItem(‘aтpибyт_yзeл’) – добавляет новый атрибут к элементу или обновляет значение у существующего. Для создания атрибута необходимо использовать метод document.createAttribute(), которому в качестве параметра необходимо передать имя атрибута. После этого созданному атрибуту необходимо присвоить значение с помощью свойства value.
- .removeNamedItem(‘имя_атрибута’) – удаляет указанный атрибут у элемента (в качестве результата возвращает удалённый атрибут).
Пример, работы с атрибутами через методы getNamedItem, setNamedItem и removeNamedItem:
<p id=»message» class=»text» style=»text-align: center;»>I LOVE JAVASCRIPT</p>
<script>
// получим элемент по его идентификатору message
var message = document.querySelector(‘#message’);
// получим его аттрибуты
var attrs = message.attributes;
// Задача №1. Получим значение атрибута id
console.log(attrs.getNamedItem(‘id’));
// Задача №2. Установим атрибут (если он есть, то изменим его значение, в противном случае добавим новый)
// создаём атрибут style и сохраняем его в переменную attrStyle
var attrStyle = document.createAttribute(‘style’);
// присваиваем значение атрибуту с помощью свойства value
attrStyle.value = ‘text-align: left;’;
// устанавливаем атрибут элементу
attrs.setNamedItem(attrStyle);
// Задача №3. удалить атрибут class у элемента
attrs.removeNamedItem(«class»);
</script>
Задачи
- Вывести в консоль все элементы документа, которые имеют атрибут id.
- Добавить атрибут title ко всем изображениям на странице, если данного атрибута у них нет. Значение атрибута установить равным значению атрибута alt.
В случае JavaScript-движка V8 — очень даже. В этой статье я привожу результаты своего маленького исследования эффективности одной из внутренних оптимизаций V8.
Описание механизма V8
Прежде чем продолжить чтение, я советую вам прочитать О внутреннем устройстве V8 и оптимизации кода чтобы лучше понимать суть происходящего.
Если очень упростить, то V8 строит объекты используя внутренние скрытые классы. Каждый такой класс соответствует уникальной структуре объекта. Например, если у нас есть такой код:
// Создаётся базовый скрытый класс, с условным названием C0
const obj = {}
// Создается новый класс с описанием поля «a», условным названием С1 и ссылкой на С0
obj.a = 1
// Создается новый класс с описанием поля «b», условным названием С2 и ссылкой на С1
obj.b = 2
// Создается новый класс с описанием поля «c», условным названием С3 и ссылкой на С2
obj.c = 3
Движок кэширует эти цепочки классов, и если вы повторно создадите объект с такими же именами полей и таким же порядком, то он не будет создавать скрытые классы заново, а будет использовать существующие. При этом сами значения свойств могут отличаться.
// Базовый класс уже существует — C0
const obj2 = {}
// Класс описывающий поле «a» и ссылающийся на C0 уже существует — С1
obj2.a = 4
// Класс описывающий поле «b» и ссылающийся на C1 уже существует — С2
obj2.b = 5
// Класс описывающий поле «c» и ссылающийся на C2 уже существует — С3
obj2.c = 6
Тем не менее это поведение легко сломать. Достаточно определить такой же объект, но с полями в другом порядке
// Базовый класс уже существует — C0
const obj3 = {}
// Не существует класса описывающего поле «c» и ссылающегося на C0.
// Будет создан новый — С4
obj3.c = 7
// Не существует класса описывающего поле «a» и ссылающегося на C4.
// Будет создан новый — С5
obj3.a = 8
// Не существует класса описывающего поле «a» и ссылающегося на C5.
// Будет создан новый — С6
obj3.b = 9
Далее я постарался протестировать и определить разницу в производительности при работе с объектами в которых имена полей совпадают и в которых они отличаются.
Метод тестирования
Я провел три группы тестов:
- Static — имена свойств и их порядок не изменялись.
- Mixed — изменялась только половина свойств.
- Dynamic — все свойства и их порядок были уникальными для каждого объекта.
- Все тесты запускались на NodeJS версии 13.7.0.
- Каждый тест запускался в отдельном, новом потоке.
- Все ключи объектов имеют одинаковую длину, а все свойства одинаковое значение.
- Для измерения времени выполнения использовался Performance Timing API.
- В показателях могут быть незначительные колебания. Это вызвано разными фоновыми процессами исполняемыми на машине в тот момент.
- Код выглядит следующим образом. Ссылку на весь проект дам ниже.const keys = getKeys();
performance.mark(‘start’);
const obj = new createObject(keys);
performance.mark(‘end’);
performance.measure(`${length}`, ‘start’, ‘end’);
Результаты
Время на создание одного объекта
Первый, самый главный и простой тест: я взял цикл на 100 итераций. В каждой итерации я создавал новый объект и измерял время на его создание для каждой итерации.
График времени на создание одного объекта в зависимости от итерации
Как видите, во время первого, «холодного» запуска во всех группах создание объекта занимает практически идентичное время. Но уже на второй-третьей итерации скорость выполнения в группах static и mixed (там, где V8 может применить оптимизацию) по сравнению с группой dynamic значительно вырастает.
И это приводит к тому, что благодаря внутренней оптимизации V8 100-й объект из группы static создаётся в 7 раз быстрее.
Или, можно сказать по другому:
не соблюдение порядка объявления свойств в идентичных объектах может привести к семикратному замедлению вашего кода.
Результат не зависит от того как именно вы создаёте объект. Я испытал несколько разных способов и все они дали практически идентичные значения.
const obj = {}
for (let key of keys) obj[key] = 42
// Тоже самое что и первый вариант, но обёрнуто в функцию-конструктов
const obj = new createObject(keys)
const obj = Object.fromEntries(entries)
const obj = eval(‘({ … })’)
Суммарное время создания нескольких объектов
В первом тесте мы обнаружили, что объект из группы dynamic может создаваться на 30-40 микросекунд дольше. Но это только один объект. А в реальных приложениях их могут быть сотни или тысячи. Подсчитаем суммарные накладные расходы в разных масштабах. В следующем тесте я буду последовательно повторять первый, но замерять не время на создание одного объекта, а суммарное время на создание массива таких объектов.
График времени на создание массива объектов в зависимости от его размера
Как видите, в масштабах всего приложения простая внутренняя оптимизация V8 может дать ускорение в десятки раз.
Где это важно
Повсюду. В JavaScript объекты повсюду. Вот несколько жизненных примеров, где соблюдение порядка свойств даст прирост производительности:
При работе с Fetch
fetch(url1, {headers, method, body})
fetch(url2, {method, headers, body})
При работе с jQuery
$.css({ color, margin })
$.css({ margin, color })
При работе с Vue
Vue.component(name1, {template, data, computed})
Vue.component(name2, {data, computed, template})
При работе с паттерном Composite (компоновщик)
const createAlligator = () => ({…canEat(), …canPoop()})
const createDog = () => ({…canPoop(), …canEat()})
И в огромном множестве других мест.
Простое соблюдение порядка свойств в объектах одного типа сделает ваш код более производительным.
Ссылки
Код тестов и подробные результаты
О внутреннем устройстве V8 и оптимизации кода