- Методы поиска элементов
- XPath в современных браузерах
- Результаты
getElements*— живые! - Практика
- Итого
Прямая навигация от родителя к потомку удобна, если элементы рядом. А если нет?
Для этого в DOM есть дополнительные методы поиска.
Методы поиска элементов
document.getElementById(id)
У каждого DOM-элемента может быть атрибут id. Его значение должно быть уникальным для документа.
Браузер поддерживает у себя внутреннее соответствие id <-> элемент, которое позволяет очень быстро получать элемент вызовом document.getElementById(id).
Например:
<body>
<div id="info">Информация</div>
<script>
var div = document.getElementById('info');
alert( div.innerHTML );
</script>
</body>
Важно что в документе может быть только один элемент с данным id. Конечно, можно нарушить это правило и создать много элементов с одинаковым идентификатором, но в таком случае поведение метода getElementById будет непредсказуемым.
Лучше придерживаться стандарта: один id — один элемент.
id
Почти все браузеры автоматически создают переменные для элементов с id.
Для примера, запустите следующий код в IE/Chrome/Opera. Выведется элемент.
<div id="a">тест</div> <script> alert(a) </script>
Но такой способ не кросс-браузерный и сохранился как отголосок далекого прошлого.
elem.getElementsByTagName(tag)
Этот метод ищет все элементы с заданным именем tag внутри элемента elem и возвращает их в виде списка. Регистр тега не имеет значения.
Можно искать и в элементе и в документе:
// получить все div-элементы
var elements = document.getElementsByTagName('div');
Найдем все элементы input внутри таблицы:
<table id="myTable">
<tr>
<td>Ваш возраст:</td>
<td>
<label>
<input type="radio" name="age" value="young" checked/> младше 18
</label>
<label>
<input type="radio" name="age" value="mature"/> от 18 до 50
</label>
<label>
<input type="radio" name="age" value="senior"/> старше 60
</label>
</td>
</tr>
</table>
<script>
*!*
var tableElem = document.getElementById('myTable');
var elements = tableElem.getElementsByTagName('input');
*/!*
for (var i=0; i<elements.length; i++) {
var input = elements[i];
alert(input.value + ': ' + input.checked);
}
</script>
Можно получить все элементы, передав звездочку '*' вместо тега:
// получить все элементы документа
var allElems = document.getElementsByTagName('*');
Если хочется получить только один элемент — можно указать индекс сразу же:
var element = document.getElementsByTagName('input')*!*[0]*/!*
document.getElementsByName(name)
Для элементов, которые поддерживают атрибут name, есть возможность получить их по этому атрибуту.
Например, все элементы с именем age:
var elems = document.getElementsByName('age');
elem.getElementsByClassName(className)
Поддерживается всеми современными браузерами, кроме IE<9.
Ищет все элементы с данным классом. Находит элемент в том числе если у элемента несколько классов, а искомый - один из них.
Запустите этот пример в любом браузере, кроме IE<9.
<div class="a b c">Несколько классов</div>
<script>
alert( document.getElementsByClassName('a')[0].innerHTML );
</script>
Как и getElementsByTagName, этот метод может быть вызван из DOM-элемента или документа.
elem.querySelectorAll(css)
Метод querySelectorAll позволяет выбирать элементы с помощью CSS3-селектора.
Он работает во всех современных браузерах, но в IE8+ — с некоторыми ограничениями:
- IE8 должен быть именно в режиме IE8, а не в режиме совместимости.
- Это не CSS 3, но CSS 2.1 для IE. Не так мощно, конечно, но этого хватает для большинства случаев.
Следующий запрос получает все элементы LI, которые являются последними потомками своих UL. Это будет работать и в IE8.
<ul>
<li>Этот</li>
<li>тест</li>
</ul>
<ul>
<li>полностью</li>
<li>пройден</li>
</ul>
<script>
*!*
var elements = document.querySelectorAll('UL > LI:last-child');
*/!*
for (var i=0; i<elements.length; i++) {
alert(elements[i].innerHTML ); // "тест", "пройден"
}
</script>
elem.querySelector(css)
То же самое, что elem.querySelectorAll(css), но возвращает только первый элемент.
Фактически, эквивалентен elem.querySelectorAll(css)[0], но в случае когда элементов много — это будет медленно, зачем искать их все, если нужен только первый? Лучше использовать querySelector.
XPath в современных браузерах
Для поиска в XML-документах существует язык запросов XPath.
Он очень мощный, во многом мощнее CSS, но сложнее. Например, запрос для поиска элементов H2, содержащих текст ‘XPath’: //h2[contains(text(),'XPath').
Все современные браузеры, кроме IE, поддерживают XPath с синтаксисом, близким к описанному в MDN.
Найдем такие заголовки в текущем документе:
var result = document.evaluate("//h2[contains(text(),'XPath')]", document.documentElement, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
for (var i=0; i<result.snapshotLength; i++) {
alert(result.snapshotItem(i).innerHTML);
}
IE тоже поддерживает XPath, но эта поддержка не соответствует стандарту и работает только для XML-документов, например, полученных с помощью XMLHTTPRequest (AJAX).
Результаты getElements* — живые!
Все DOM-запросы, которые начинаются с getElements.., возвращают живые списки узлов.
При изменении документа — изменяется и результат запроса. Например:
<div></div>
<script>
var elems = document.body.getElementsByTagName('div');
alert(elems.length); // 1
document.body.innerHTML = ''; // удалить все из BODY
alert(elems.length); // 0
</script>
Дело в том, что результат таких запросов — это не массив, а коллекция, имеющая тип NodeList или HTMLCollection.
У коллекции есть индексы, длина, но нет push, pop и других свойств массива.
При обращении к элементам такой коллекции, поиск выполняется каждый раз заново. Другими словами, это не коллекция, а «живой поисковой запрос». Поэтому после удаления всего содержимого body значение elems.length=0.
Еще одно важное следствие. Такой повторный поиск оптимизируется движком браузера, но в любом случае —- работа с коллекцией медленнее, чем с обычным массивом.
Живые — только коллекции, возвращенные методами getElements*. Сказать по правде, это даже не коллекции, а живые запросы, поиск по которым происходит при обращении к i-му элементу (или свойству length). У браузера есть внутреннее кеширование для них, но кеш сбрасывается при изменении документа.
Метод querySelectorAll, напротив, ищет сразу и возвращает статический список, который автоматически не обновляется.
Вот небольшой документ:
<ul id="menu"> <li>Главная страница</li> <li>Форум</li> <li>Магазин</li> </ul>
1. Что выведет следующий код (простой вопрос)?
var lis = document.body.getElementsByTagName('li');
document.body.innerHTML = '';
alert(lis.length);
2. А такой код (вопрос посложнее)?
var lis = document.getElementById('menu').getElementsByTagName('li');
document.body.innerHTML = '';
alert(lis.length);
1. Ответ на первый вопрос - 0, пустая коллекция.
<ul id="menu">
<li>Главная страница</li>
<li>Форум</li>
<li>Магазин</li>
</ul>
<script>
var lis = document.body.getElementsByTagName('li');
document.body.innerHTML = '';
alert(lis.length);
</script>
Это потому, что все элементы из BODY удаляются, а коллекция - живая.
2. Ответ на второй вопрос - 3, коллекция не изменилась:
<ul id="menu">
<li>Главная страница</li>
<li>Форум</li>
<li>Магазин</li>
</ul>
<script>
var lis = document.getElementById('menu').getElementsByTagName('li');
document.body.innerHTML = '';
alert(lis.length);
</script>
В отличие от предыдущего примера, теперь коллекция привязана не к BODY, а к элементу, в котором идет поиск, т.е. к <ul id="menu">.
Присвоение document.body.innerHTML='' удаляет узлы из document.body, почему же они остались в коллекции?
Дело в том, что удаленные узлы остаются «в живых» до тех пор, пока есть ссылка на них из какого-нибудь объекта или переменной.
В данном случае к узлу <ul id="menu"> привязана коллекция, на которую ссылается переменная lis:
lis -> коллекция -> узел <ul id="menu">
Поэтому узел UL остается в памяти, и все потомки в месте с ним. Как следствие, длина коллекции по-прежнему равна 3.
Практика
Задачи ниже будут использовать следующий HTML-код:
<!DOCTYPE HTML>
<html>
<head><meta charset="utf-8"></head>
<body>
<form name="search">
<label>Искать: <input type="text" name="search"></label>
</form>
<form name="age-form" id="age-form">
<table id="age-table">
<tr>
<td>Возраст:</td>
<td id="age-list">
<label><input type="radio" name="age" value="young"> до 18</label>
<label><input type="radio" name="age" value="mature"> 18-50</label>
<label><input type="radio" name="age" value="senior"> более 50</label>
</td>
</tr>
</table>
</form>
</body>
</html>
В документе tutorial/browser/dom/searchTask.html..
Используя методы поиска getElements* найдите:
- Все элементы
labelвнутри таблицы. Должно быть 3 элемента. - Первую ячейку таблицы (содержит слово
"Возраст"). inputв форме с именемsearch.
При решении нельзя использовать никакие методы кроме getElement*.
Решение:
-
var table = document.getElementById('age-table'); var result = table.getElementsByTagName('label'); -
var table = document.getElementById('age-table'); var result = table.getElementsByTagName('td')[0]; -
var searchForm = document.getElementsByName('search')[0]; var result = searchForm.getElemensByTagName('input')[0];
Для документа tutorial/browser/dom/searchTask.html.
Напишите функцию checkInsideTable(id), которая будет возвращать true если элемент с данным id находится внутри таблицы table#age-table.
Если это не так, или такого элемента нет, то функция должна вернуть false.
Например:
checkInsideTable('age-list'); // true
checkInsideTable('age-form'); // false
checkInsideTable('non-existant-id'); // false
Для начала, нам нужно получить таблицу и элемент по id.
var elem = document.getElementById(id);
var table = document.getElementById('age-table');
Затем, мы должны проверить, является ли table предком elem. Это можно сделать, пройдя по цепочке родительских элементов: elem.parentNode, elem.parentNode.parentNode.., пока не дойдем либо до таблицы либо до null.
Код рабочей функции:
function checkInsideTable(id){
var elem = document.getElementById(id);
var table = document.getElementById('age-table');
while (elem != table && elem) { // вверх по родителям
elem = elem.parentNode;
}
return !!elem; // приведение логическому типу (true если не null)
}
Отобразите число потомков
Есть дерево: tutorial/browser/dom/treeSource.html.
Напишите код, который добавит каждому элементу списка(LI) количество вложенных в него элементов. Те элементы, которые не содержат в себе других элементов - пропускайте.
Добавьте ваш код в конец BODY.
Результат:
.
Сперва, опишем алгоритм решения.
Он может быть таким:
- Найти все элементы списка.
- Для каждого элемента списка:
- Подсчитать количество потомков
LI. - Если количество равно 0, пропускаем этот элемент, иначе, изменяем DOM, добавляя эту информацию.
- Подсчитать количество потомков
Реализуйте его.
Количество потомков можно получить как UL.getElementsByTagName('li').length.
Как добавить текст с количеством потомков к LI ?
Заголовок узла дерева(первый потомок LI) — это текстовый узел. Добавить к нему текст можно с помощью свойства data.
Решение: tutorial/browser/dom/tree.html.
Дополнительно
Для любого документа, сделаем следующее:
var aList1 = document.getElementsByTagName('a'),
var aList2 = document.querySelectorAll('a');
document.body.appendChild(document.createElement('a'));
alert(aList1.length - aList2.length);
Каков будет результат? Почему?
Результатом будет 1, потому что getElementsByTagName - живая коллекция, которая автоматически дополнилась новым элементом a и ее длина увеличилась на 1.
А вот querySelector, наоборот, возвращает статичный список узлов. Он ссылается на те же самые элементы, что бы не происходило с документом. Вот поэтому его длина и остается неизменной.
Какой метод поиска элементов работает быстрее: getElementsByTagName(tag) или querySelectorAll(tag)?
Напишите код, который измеряет разницу между ними.
Исходный документ: tutorial/browser/dom/speed-selector-src/index.html
P.S. В задаче есть подвох, все не так просто. Если разница больше 10 раз — вы решили ее неверно. Тогда подумайте, почему такое может быть.
Для бенчмаркинга будем использовать функцию bench(f, times), которая запускает функцию f times раз и возвращает разницу во времени:
function bench(f, times) {
var d = new Date();
for(var i=0; i<times; i++) f();
return new Date() - d;
}
Первый вариант (неверный) — замерять разницу между функциями runGet/runQuery, вот так:
function runGet() {
var results = document.getElementsByTagName('p');
}
function runQuery() {
var results = document.querySelectorAll('p');
}
alert( bench(runGet, 10000) ); // вывести время 1000*runGet
Он даст неверные результаты, т.к. getElementsByTagName является «живым поисковым запросом». Если не обратиться к его результатам, то поиска не произойдет вообще, т.е. runGet ничего по сути не ищет.
…А querySelectorAll всегда производит поиск и формирует список элементов.
Более правильный тест — это не только запустить поиск, но и получить все элементы, как это делается в реальной жизни.
Полное решение: tutorial/browser/dom/speed-selector/index.html
Итого
Есть 5 основных способов получения элементов DOM:
| Метод | Ищет по.. | Ищет внутри элемента? | Поддержка |
getElementById |
id | - | везде |
getElementsByName |
name | - | везде |
getElementsByTagName |
tag/* | ✔ | везде |
getElementsByClassName |
class | ✔ | везде, IE9+ |
querySelector(All) |
CSS-селектор | ✔ | везде, CSS 2.1 в IE8+ |
XPath, хоть и поддерживается большинством браузеров, но используется редко.
Комментарии
- Приветствуются комментарии, содержащие дополнения и вопросы по статье, и ответы на них.
- Если ваш комментарий касается задачи -- откройте её в отдельном окне и напишите там.
- Комментарии без смысла, с рекламой или не о статье вообще - удаляются.