原文链接:https://javascript.info/searching-elements-dom,translate with ❤️ by zhangbao.

对于相互接近的元素,使用 DOM 提供的遍历属性,操作起来非常方便。那么当我们想要获取页面上的任意一个元素时,该怎么做呢?

DOM 也有提供这样的查找方法。

document.getElementById 或直接使用 id

如果一个元素具有 id 特性值,那么对应就存在一个同名全局变量。

我们可以用这个变量访问元素:

  1. <div id="elem">
  2. <div id="elem-content">Element</div>
  3. </div>
  4. <script>
  5. alert(elem); // 这里的 elem 就代表 <div id="elem"> 元素
  6. alert(window.elem); // window.elem 等同于 elem
  7. // 想要获取 id 是 elem-content 的元素
  8. // 必须使用 [] 的形式,因为 elem-content 不是一个有效的标识符
  9. alert(window['elem-content']);
  10. </script>

但这种方式也存在一些潜在问题,比如被我们一不小心的覆盖了。

  1. <div id="elem"></div>
  2. <script>
  3. let elem = 5;
  4. alert(elem); // 5
  5. </script>

这个行为在规范中描述,只是为了向后兼容。而且我们已经看到,这也可能带来一些诸如变量覆盖,不能清除判断一个变量表示的是哪个 DOM 节点的问题。

所以,最好的选择是使用 document.getElementById(id) 方法,手动获取 DOM 元素。

例如:

  1. <div id="elem">
  2. <div id="elem-content">Element</div>
  3. </div>
  4. <script>
  5. let elem = document.getElementById('elem');
  6. elem.style.background = 'red';
  7. </script>

在本教程中,我们通常使用 id 直接引用元素,目的只是为了保证代码尽量简短而已。在真实的开发环境下,还是推荐使用 document.getElementById 方法。

查找:getElement* 和 querySelector* - 图1id 值是唯一的

在一个文档中,一个 id`` 值必定在这个文档中是唯一的。

如果一个文档中存在多个 id值一样的元素,那么使用 getElementById 方法查询的结果将是不可预料的。浏览器可能返回它们中的任意一个,所以千万要确保 id 值的唯一性。

查找:getElement* 和 querySelector* - 图2getElementById** 仅是 ****document** 对象上的方法

不存在 anyNode.getElementById方法,getElementById 只能在 docuemnt`` 上使用——它查找文档范围内匹配给定 id 名的元素。

getElementsBy*

除了 getElementById,还提供了其他查找节点的方法:

  • elem.getElementsByTagName(tag): 返回指定标签的元素集合。tag 可以是“*”也可以是“任意一个标签名”。
  1. // 获得文档中所有的 div
  2. let divs = document.getElementsByTagName('div');

这个方法也可以在 DOM 元素上调用。例如,查找表格里的所有 input

  1. <table id="table">
  2. <tr>
  3. <td>Your age:</td>
  4. <td>
  5. <label>
  6. <input type="radio" name="age" value="young" checked> less than 18
  7. </label>
  8. <label>
  9. <input type="radio" name="age" value="mature"> from 18 to 50
  10. </label>
  11. <label>
  12. <input type="radio" name="age" value="senior"> more than 60
  13. </label>
  14. </td>
  15. </tr>
  16. </table>
  17. <script>
  18. let inputs = table.getElementsByTagName('input');
  19. for (let input of inputs) {
  20. alert( input.value + ': ' + input.checked );
  21. }
  22. </script>

查找:getElement* 和 querySelector* - 图3 不要忘记字符 “s”``

开发者经常忘记字符 “s”。也就是会错误调用 getElementByTagName 而不是 getElementsByTagName

字符 “s”没有出现在 getElementById 中,是因为这个方法返回的始终是单个元素。但是 getElementsByTagName返回的是元素集合,因此才有 "s" 字符在其中。

查找:getElement* 和 querySelector* - 图4 返回的是集合,而不是单个元素!

另一个普遍的新手错误是写成这样:

  1. // 这是错误的
  2. document.getElementsByTagName('input').value = 5;

这是行不通的,因为它获得的是一组输入框的集合,这是在给集合赋值,而不是集合内部的元素节点。

我们应该迭代集合或通过索引获取元素,然后赋值,像下面这样:

  1. // 这样写 OK (如果这里有 input 的话)
  2. document.getElementsByTagName('input')[0].value = 5;

还有两个一类但较少使用的方法:

  • elem.getElementsByClassName(className):返回给定类名下的元素集合。

  • document.getElementsByName:返回给定 name 特性值的元素集合(很少使用,这里提到只是为了教程的完整性)。

例如:

  1. <form name="my-form">
  2. <div class="article">文章</div>
  3. <div class="article--long">长文章</div>
  4. </form>
  5. <script>
  6. // 获得包含给定 name 特性值的元素
  7. let form = document.getElementByName('my-form')[0];
  8. // 在 form 下寻找包含给定类名的元素集合
  9. let articles = form.getElementsByClassName('article');
  10. alert(articles.length); // 2。发现 2 个包含类名“article”的元素
  11. </script>

下面迎来重磅选手 querySelectorAll

querySelectorAll

调用 elem.querySelectorAll(css) 返回 elem 中匹配指定 CSS 选择器的所有元素组成的集合。这是最常使用的一个强大方法。

举个例子:我们查找由 ul 下最后一个 li 孩子元素组成的集合。

  1. <ul>
  2. <li>The</li>
  3. <li>test</li>
  4. </ul>
  5. <ul>
  6. <li>has</li>
  7. <li>passed</li>
  8. </ul>
  9. <script>
  10. let elements = document.querySelectorAll('ul li:last-child');
  11. for (let elem of elements) {
  12. alert(elem.innerHTML); // "test" "passed"
  13. }
  14. </script>

这个方法非常强大,因为可以使用任何 CSS 选择器。

查找:getElement* 和 querySelector* - 图5也支持伪类

: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 选择器,返回 truefalse

在我们需要遍历元素集合,过滤出我们感兴趣的元素时,这个方法用起来还是很顺手的。

例如:

  1. <a href="http://example.com/file.zip">...</a>
  2. <a href="http://ya.ru">...</a>
  3. <script>
  4. // 找出 document.body.children 中匹配 a[href$="zip"] 的元素
  5. for (let elem of document.body.children) {
  6. if (elem.matches('a[href$="zip"]')) {
  7. alert("存档参考: " + elem.href );
  8. }
  9. }
  10. </script>

closest

所以元素的上属元素都称为祖先元素。也就是说,祖先就是父级、父级的父级……一级一级的父级及元素本身组成了一个祖先链。

elem.closest(css) 查找并返回最近的匹配给定的 CSS 选择器的祖先元素, elem 元素本身也在匹配之列。

也就是说,closest 方法从自身开始一路往上检查每一个父级元素,如果匹配了给定的选择器,就停止查找,返回该元素。

例如:

  1. <h1>内容</h1>
  2. <div class="contents">
  3. <ul class="book">
  4. <li class="chapter">第一章</li>
  5. <li class="chapter">第二章</li>
  6. </ul>
  7. </div>
  8. <script>
  9. let chapter = document.querySelector('.chapter'); // LI
  10. alert(chapter.closest('.book')); // UL
  11. alert(chapter.closest('.contents')); // DIV
  12. alert(chapter.closest('h1')); // null (因为 h1 不是祖先元素)
  13. </script>

实时集合

所有的 "getElementsBy*" 的方法返回的都是实时集合。这个集合总是能实时反映文档当前的元素状态,当元素改变时这个集合也会“自动更新”。

下面例子里,有两个脚本。

  1. 第一个脚本创建一个变量,引用了所有 <div> 元素。现在 divslength 值是 1

  2. 第二个脚本在浏览器多解析一个 <div> 后执行,divslength 值变成了 2

  1. <div>第一个 div</div>
  2. <script>
  3. let divs = document.getElementsByTagName('div');
  4. alert(divs.length); // 1
  5. </script>
  6. <div>第二个 div</div>
  7. <script>
  8. alert(divs.length); // 2
  9. </script>

getElementsBy* 不同的是,querySelectorAll 返回的是一个静态集合,就是一个固定的元素集合。如果把上面例子的方法换成 querySelectorAll,两个脚本输出结果都是 1

  1. <div>第一个 div</div>
  2. <script>
  3. let divs = document.querySelectorAll('div');
  4. alert(divs.length); // 1
  5. </script>
  6. <div>第二个 div</div>
  7. <script>
  8. alert(divs.length); // 1
  9. </script>

现在我们能容易区分不同点了。静态集合即使在文档中新增了一个 <div> 后,仍是不变的。

在这里,我们使用分开的脚本来说明添加元素是如何影响集合的,但是任何 DOM 操作都会影响它们,很快我们就会看到。

总结

下面列举了查找 DOM 节点的 6 个主要方法:

方法 通过……查找 可以在 elem 上调用吗? 实时?
getElementById id -
getElementsByName name -
getElementsByTagName 标签名或 "*"
getElementsByClassName 类名
querySelector CSS 选择器 -
querySelectorAll CSS 选择器 -

请注意,getElementByIdgetElementsByName 只能在 document 上调用。

其他方法都可以在元素上调用。比如 elem.querySelectorAll(...) 查找 elem 中匹配给定选择器的元素集合。

除此之外,还提供了:

  • elem.matches(css) 检查 elem 是否匹配给定 CSS 选择器,返回 truefalse

  • elem.closest(css) 查找匹配给定的 CSS 选择器的最近祖先元素,elem 元素本身也在查找之列。

还要提到一个方法用来检查父子关系:

  • elemA.contains(elemB):检查 elemA 中是否包含/等于 elemB

查找:getElement* 和 querySelector* - 图6IE 浏览器的兼容性

  1. elem.matches(css) 在 IE 中不支持,需要使用非标准方法 msMatchesSelector 替代,

  2. elem.closest(css) 方法 IE 不支持,

  3. elemA.contains(elemB) IE 全面支持(仅不支持在 document 上使用),

练习题

问题

一、查找元素

有一个文档,包含表格和表单。

怎么查找?

  1. id="age-table" 的表格。

  2. 表格里的所有 label 元素(有 3 个)。

  3. 表格中的第一个 td 元素。

  4. name="search"form

  5. 表单的第一个 input

  6. 表单的最后一个 input

二、统计后代数量

有一个内嵌的 ul/li 树结构。

  1. <ul>
  2. <li>Animals
  3. <ul>
  4. <li>Mammals
  5. <ul>
  6. <li>Cows</li>
  7. <li>Donkeys</li>
  8. <li>Dogs</li>
  9. <li>Tigers</li>
  10. </ul>
  11. </li>
  12. <li>Other
  13. <ul>
  14. <li>Snakes</li>
  15. <li>Birds</li>
  16. <li>Lizards</li>
  17. </ul>
  18. </li>
  19. </ul>
  20. </li>
  21. <li>Fishes
  22. <ul>
  23. <li>Aquarium
  24. <ul>
  25. <li>Guppy</li>
  26. <li>Angelfish</li>
  27. </ul>
  28. </li>
  29. <li>Sea
  30. <ul>
  31. <li>Sea trout</li>
  32. </ul>
  33. </li>
  34. </ul>
  35. </li>
  36. </ul>

书写代码展示每一个 <li> 的内容:

  1. 里面的文本是什么(不需要输出子树)。

  2. 嵌套子元素 <li> 的数量——所有后代,包括深层嵌套的后代。

答案

一、

  1. // 1. id="age-table" 的表格
  2. let table = document.getElementById('age-table');
  3. // 或者
  4. document.querySelector('#age-table');
  5. // 2. 表格里的所有 label 元素(有 3 个)
  6. table.getElementsByTagName('label');//
  7. // 或者
  8. document.querySelectorAll('#age-table label');
  9. // 3. 表格中的第一个 td 元素
  10. table.rows[0].cells[0]
  11. // 或者
  12. table.getElementsByTagName('td')
  13. // 或者
  14. table.querySelector('td')
  15. // 4. name="search" 的 form
  16. let form = document.getElementsByName('search')[0]
  17. // 或者
  18. document.querySelector('form[name="search"]')
  19. // 5. 表单的第一个 input
  20. form.getElementsByTagName('input')[0]
  21. // 或者
  22. form.querySelector('input')
  23. // 6. 表单的最后一个 input
  24. let inputs = form.querySelectorAll('input') // 查找所有
  25. inputs[inputs.length-1] // 拿最后一个
  26. // 或者
  27. form.querySelector('input:last-child');

二、

  1. for (let li of document.querySelectorAll('li')) {
  2. // 从文本节点获得标题
  3. let title = li.firstChild.data;
  4. title = title.trim(); // 删除结尾处的额外空格
  5. // 统计后代 li 元素的数量
  6. let count = li.getElementsByTagName('li').length;
  7. alert(title + ': ' + count);
  8. }

(完)