原文链接:https://javascript.info/searching-elements-dom,translate with ❤️ by zhangbao.
对于相互接近的元素,使用 DOM 提供的遍历属性,操作起来非常方便。那么当我们想要获取页面上的任意一个元素时,该怎么做呢?
DOM 也有提供这样的查找方法。
document.getElementById 或直接使用 id
如果一个元素具有 id
特性值,那么对应就存在一个同名全局变量。
我们可以用这个变量访问元素:
<div id="elem">
<div id="elem-content">Element</div>
</div>
<script>
alert(elem); // 这里的 elem 就代表 <div id="elem"> 元素
alert(window.elem); // window.elem 等同于 elem
// 想要获取 id 是 elem-content 的元素
// 必须使用 [] 的形式,因为 elem-content 不是一个有效的标识符
alert(window['elem-content']);
</script>
但这种方式也存在一些潜在问题,比如被我们一不小心的覆盖了。
<div id="elem"></div>
<script>
let elem = 5;
alert(elem); // 5
</script>
这个行为在规范中描述,只是为了向后兼容。而且我们已经看到,这也可能带来一些诸如变量覆盖,不能清除判断一个变量表示的是哪个 DOM 节点的问题。
所以,最好的选择是使用 document.getElementById(id)
方法,手动获取 DOM 元素。
例如:
<div id="elem">
<div id="elem-content">Element</div>
</div>
<script>
let elem = document.getElementById('elem');
elem.style.background = 'red';
</script>
在本教程中,我们通常使用 id
直接引用元素,目的只是为了保证代码尽量简短而已。在真实的开发环境下,还是推荐使用 document.getElementById
方法。
id 值是唯一的
在一个文档中,一个 id`` 值必定在这个文档中是唯一的。
如果一个文档中存在多个 id
值一样的元素,那么使用 getElementById
方法查询的结果将是不可预料的。浏览器可能返回它们中的任意一个,所以千万要确保 id 值的唯一性。
getElementById
** 仅是 ****document**
对象上的方法不存在 anyNode.getElementById
方法,getElementById
只能在 docuemnt`` 上使用——它查找文档范围内匹配给定 id 名的元素。
getElementsBy*
除了 getElementById
,还提供了其他查找节点的方法:
elem.getElementsByTagName(tag)
: 返回指定标签的元素集合。tag
可以是“*
”也可以是“任意一个标签名”。
// 获得文档中所有的 div
let divs = document.getElementsByTagName('div');
这个方法也可以在 DOM 元素上调用。例如,查找表格里的所有 input
:
<table id="table">
<tr>
<td>Your age:</td>
<td>
<label>
<input type="radio" name="age" value="young" checked> less than 18
</label>
<label>
<input type="radio" name="age" value="mature"> from 18 to 50
</label>
<label>
<input type="radio" name="age" value="senior"> more than 60
</label>
</td>
</tr>
</table>
<script>
let inputs = table.getElementsByTagName('input');
for (let input of inputs) {
alert( input.value + ': ' + input.checked );
}
</script>
不要忘记字符 “s”``!
开发者经常忘记字符 “s”
。也就是会错误调用 getElementByTagName 而不是 getElementsByTagName
。字符 “s”
没有出现在 getElementById
中,是因为这个方法返回的始终是单个元素。但是 getElementsByTagName返回的是元素集合,因此才有 "s"
字符在其中。
返回的是集合,而不是单个元素!
另一个普遍的新手错误是写成这样:
// 这是错误的
document.getElementsByTagName('input').value = 5;
这是行不通的,因为它获得的是一组输入框的集合,这是在给集合赋值,而不是集合内部的元素节点。
我们应该迭代集合或通过索引获取元素,然后赋值,像下面这样:
// 这样写 OK (如果这里有 input 的话)
document.getElementsByTagName('input')[0].value = 5;
还有两个一类但较少使用的方法:
elem.getElementsByClassName(className)
:返回给定类名下的元素集合。document.getElementsByName
:返回给定name
特性值的元素集合(很少使用,这里提到只是为了教程的完整性)。
例如:
<form name="my-form">
<div class="article">文章</div>
<div class="article--long">长文章</div>
</form>
<script>
// 获得包含给定 name 特性值的元素
let form = document.getElementByName('my-form')[0];
// 在 form 下寻找包含给定类名的元素集合
let articles = form.getElementsByClassName('article');
alert(articles.length); // 2。发现 2 个包含类名“article”的元素
</script>
下面迎来重磅选手 querySelectorAll
。
querySelectorAll
调用 elem.querySelectorAll(css)
返回 elem
中匹配指定 CSS 选择器的所有元素组成的集合。这是最常使用的一个强大方法。
举个例子:我们查找由 ul
下最后一个 li
孩子元素组成的集合。
<ul>
<li>The</li>
<li>test</li>
</ul>
<ul>
<li>has</li>
<li>passed</li>
</ul>
<script>
let elements = document.querySelectorAll('ul li:last-child');
for (let elem of elements) {
alert(elem.innerHTML); // "test" "passed"
}
</script>
这个方法非常强大,因为可以使用任何 CSS 选择器。
也支持伪类
像
:hover
和:active
这样的伪类选择器,也受到支持。例如,document.querySelectorAll(':hover')
会返回当前所有处于 hover 状态的元素组成的集合(按照嵌套嵌套顺序,从最外层的 到最里层的嵌套元素)。
querySelector
elem.querySelector(css)
返回匹配给定 CSS 选择器的第一个元素。
也就是说,elem.querySelector(css)
的结果等同于 elem.querySelectorAll(css)[0]
。如果只查找单个元素的话,前者比后者要快而且写法简短。
matches
之前的方法都是用来查找 DOM 节点的。
而 elem.matches(css) 则是检查元素 elem
是否匹配给定的 CSS 选择器,返回 true
或 false
。
在我们需要遍历元素集合,过滤出我们感兴趣的元素时,这个方法用起来还是很顺手的。
例如:
<a href="http://example.com/file.zip">...</a>
<a href="http://ya.ru">...</a>
<script>
// 找出 document.body.children 中匹配 a[href$="zip"] 的元素
for (let elem of document.body.children) {
if (elem.matches('a[href$="zip"]')) {
alert("存档参考: " + elem.href );
}
}
</script>
closest
所以元素的上属元素都称为祖先元素。也就是说,祖先就是父级、父级的父级……一级一级的父级及元素本身组成了一个祖先链。
elem.closest(css)
查找并返回最近的匹配给定的 CSS 选择器的祖先元素, elem
元素本身也在匹配之列。
也就是说,closest
方法从自身开始一路往上检查每一个父级元素,如果匹配了给定的选择器,就停止查找,返回该元素。
例如:
<h1>内容</h1>
<div class="contents">
<ul class="book">
<li class="chapter">第一章</li>
<li class="chapter">第二章</li>
</ul>
</div>
<script>
let chapter = document.querySelector('.chapter'); // LI
alert(chapter.closest('.book')); // UL
alert(chapter.closest('.contents')); // DIV
alert(chapter.closest('h1')); // null (因为 h1 不是祖先元素)
</script>
实时集合
所有的 "getElementsBy*"
的方法返回的都是实时集合。这个集合总是能实时反映文档当前的元素状态,当元素改变时这个集合也会“自动更新”。
下面例子里,有两个脚本。
第一个脚本创建一个变量,引用了所有
<div>
元素。现在divs
的length
值是1
。第二个脚本在浏览器多解析一个
<div>
后执行,divs
的length
值变成了2
。
<div>第一个 div</div>
<script>
let divs = document.getElementsByTagName('div');
alert(divs.length); // 1
</script>
<div>第二个 div</div>
<script>
alert(divs.length); // 2
</script>
与 getElementsBy*
不同的是,querySelectorAll
返回的是一个静态集合,就是一个固定的元素集合。如果把上面例子的方法换成 querySelectorAll
,两个脚本输出结果都是 1
:
<div>第一个 div</div>
<script>
let divs = document.querySelectorAll('div');
alert(divs.length); // 1
</script>
<div>第二个 div</div>
<script>
alert(divs.length); // 1
</script>
现在我们能容易区分不同点了。静态集合即使在文档中新增了一个 <div>
后,仍是不变的。
在这里,我们使用分开的脚本来说明添加元素是如何影响集合的,但是任何 DOM 操作都会影响它们,很快我们就会看到。
总结
下面列举了查找 DOM 节点的 6 个主要方法:
方法 | 通过……查找 | 可以在 elem 上调用吗? | 实时? |
---|---|---|---|
getElementById |
id |
- | ✔ |
getElementsByName |
name |
- | ✔ |
getElementsByTagName |
标签名或 "*" |
✔ | ✔ |
getElementsByClassName |
类名 | ✔ | ✔ |
querySelector |
CSS 选择器 | ✔ | - |
querySelectorAll |
CSS 选择器 | ✔ | - |
请注意,getElementById
和 getElementsByName
只能在 document
上调用。
其他方法都可以在元素上调用。比如 elem.querySelectorAll(...)
查找 elem
中匹配给定选择器的元素集合。
除此之外,还提供了:
elem.matches(css)
检查elem
是否匹配给定 CSS 选择器,返回true
或false
。elem.closest(css)
查找匹配给定的 CSS 选择器的最近祖先元素,elem
元素本身也在查找之列。
还要提到一个方法用来检查父子关系:
elemA.contains(elemB)
:检查elemA
中是否包含/等于elemB
。
IE 浏览器的兼容性
elem.matches(css)
在 IE 中不支持,需要使用非标准方法msMatchesSelector
替代,
elem.closest(css)
方法 IE 不支持,
elemA.contains(elemB)
IE 全面支持(仅不支持在document
上使用),
练习题
问题
一、查找元素
有一个文档,包含表格和表单。
怎么查找?
id="age-table"
的表格。表格里的所有
label
元素(有 3 个)。表格中的第一个
td
元素。name="search"
的form
。表单的第一个
input
。表单的最后一个
input
。
二、统计后代数量
有一个内嵌的 ul/li
树结构。
<ul>
<li>Animals
<ul>
<li>Mammals
<ul>
<li>Cows</li>
<li>Donkeys</li>
<li>Dogs</li>
<li>Tigers</li>
</ul>
</li>
<li>Other
<ul>
<li>Snakes</li>
<li>Birds</li>
<li>Lizards</li>
</ul>
</li>
</ul>
</li>
<li>Fishes
<ul>
<li>Aquarium
<ul>
<li>Guppy</li>
<li>Angelfish</li>
</ul>
</li>
<li>Sea
<ul>
<li>Sea trout</li>
</ul>
</li>
</ul>
</li>
</ul>
书写代码展示每一个 <li>
的内容:
里面的文本是什么(不需要输出子树)。
嵌套子元素
<li>
的数量——所有后代,包括深层嵌套的后代。
答案
一、
// 1. id="age-table" 的表格
let table = document.getElementById('age-table');
// 或者
document.querySelector('#age-table');
// 2. 表格里的所有 label 元素(有 3 个)
table.getElementsByTagName('label');//
// 或者
document.querySelectorAll('#age-table label');
// 3. 表格中的第一个 td 元素
table.rows[0].cells[0]
// 或者
table.getElementsByTagName('td')
// 或者
table.querySelector('td')
// 4. name="search" 的 form
let form = document.getElementsByName('search')[0]
// 或者
document.querySelector('form[name="search"]')
// 5. 表单的第一个 input
form.getElementsByTagName('input')[0]
// 或者
form.querySelector('input')
// 6. 表单的最后一个 input
let inputs = form.querySelectorAll('input') // 查找所有
inputs[inputs.length-1] // 拿最后一个
// 或者
form.querySelector('input:last-child');
二、
for (let li of document.querySelectorAll('li')) {
// 从文本节点获得标题
let title = li.firstChild.data;
title = title.trim(); // 删除结尾处的额外空格
// 统计后代 li 元素的数量
let count = li.getElementsByTagName('li').length;
alert(title + ': ' + count);
}
(完)