20日 八月 2020

搜索:getElement*,querySelector*

当元素彼此靠得近时,DOM 导航属性(navigation property)非常有用。如果不是,那该怎么办?如何去获取页面上的任意元素?

还有其他搜索方法。

document.getElementById 或者只使用 id

如果一个元素有 id 特性(attribute),那我们就可以使用 document.getElementById(id) 方法获取该元素,无论它在哪里。

例如:

<div id="elem">
  <div id="elem-content">Element</div>
</div>

<script>
  // 获取该元素
  let elem = document.getElementById('elem');

  // 将该元素背景改为红色
  elem.style.background = 'red';
</script>

此外,还有一个通过 id 命名的全局变量,它引用了元素:

<div id="elem">
  <div id="elem-content">Element</div>
</div>

<script>
  // elem 是对带有 id="elem" 的 DOM 元素的引用
  elem.style.background = 'red';

  // id="elem-content" 内有连字符,所以它不能成为一个变量
  // ...但是我们可以通过使用方括号 window['elem-content'] 来访问它
</script>

……除非我们声明一个具有相同名称的 JavaScript 变量,否则它具有优先权:

<div id="elem"></div>

<script>
  let elem = 5; // 现在 elem 是 5,而不是对 <div id="elem"> 的引用

  alert(elem); // 5
</script>
请不要使用以 id 命名的全局变量来访问元素

在规范中 对此行为进行了描述,所以它是一种标准。但这是注意考虑到兼容性才支持的。

浏览器尝试通过混合 JavaScript 和 DOM 的命名空间来帮助我们。对于内联到 HTML 中的简单脚本来说,这还行,但是通常来说,这不是一件好事。因为这可能会造成命名冲突。另外,当人们阅读 JavaScript 代码且看不到对应的 HTML 时,变量的来源就会不明显。

在本教程中,我们只会在元素来源非常明显时,为了简洁起见,才会使用 id 直接引用对应的元素。

在实际开发中,document.getElementById 是首选方法。

id 必须是唯一的

id 必须是唯一的。在文档中,只能有一个元素带有给定的 id

如果有多个元素都带有同一个 id,那么使用它的方法的行为是不可预测的,例如 document.getElementById 可能会随机返回其中一个元素。因此,请遵守规则,保持 id 的唯一性。

只有 document.getElementById,没有 anyElem.getElementById

getElementById 方法只能被在 document 对象上调用。它会在整个文档中查找给定的 id

querySelectorAll

到目前为止,最通用的方法是 elem.querySelectorAll(css),它返回 elem 中与给定 CSS 选择器匹配的所有元素。

在这里,我们查找所有为最后一个子元素的 <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 选择器。

也可以使用伪类

CSS 选择器的伪类,例如 :hover:active 也都是被支持的。例如,document.querySelectorAll(':hover') 将会返回鼠标指针现在已经结束的元素的集合(按嵌套顺序:从最外层 <html> 到嵌套最多的元素)。

querySelector

elem.querySelector(css) 调用会返回给定 CSS 选择器的第一个元素。

换句话说,结果与 elem.querySelectorAll(css)[0] 相同,但是后者会查找 所有 元素,并从中选取一个,而 elem.querySelector 只会查找一个。因此它在速度上更快,并且写起来更短。

matches

之前的方法是搜索 DOM。

elem.matches(css) 不会查找任何内容,它只会检查 elem 是否与给定的 CSS 选择器匹配。它返回 truefalse

当我们遍历元素(例如数组或其他内容)并试图过滤那些我们感兴趣的元素时,这个方法会很有用。

例如:

<a href="http://example.com/file.zip">...</a>
<a href="http://ya.ru">...</a>

<script>
  // 不一定是 document.body.children,还可以是任何集合
  for (let elem of document.body.children) {
    if (elem.matches('a[href$="zip"]')) {
      alert("The archive reference: " + elem.href );
    }
  }
</script>

closest

元素的祖先(ancestor)是:父级,父级的父级,它的父级等。祖先们一起组成了从元素到顶端的父级链。

elem.closest(css) 方法会查找与 CSS 选择器匹配的最近的祖先。elem 自己也会被搜索。

换句话说,方法 closest 在元素中得到了提升,并检查每个父级。如果它与选择器匹配,则停止搜索并返回该祖先。

例如:

<h1>Contents</h1>

<div class="contents">
  <ul class="book">
    <li class="chapter">Chapter 1</li>
    <li class="chapter">Chapter 1</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*

还有其他通过标签,类等查找节点的方法。

如今,它们大多已经成为了历史,因为 querySelector 功能更强大,写起来更短。

因此,这里我们介绍它们只是为了完整起见,而你仍然可以在就脚本中找到这些方法。

  • elem.getElementsByTagName(tag) 查找具有给定标签的元素,并返回它们的集合。tag 参数也可以是对于“任何标签”的星号 "*"
  • elem.getElementsByClassName(className) 返回具有给定CSS类的元素。
  • document.getElementsByName(name) 返回在文档范围内具有给定 name 特性的元素。很少使用。

例如:

// 获取文档中的所有 div
let divs = document.getElementsByTagName('div');

让我们查找 table 中的所有 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

getElementById 中没有字母 "s",是因为它只返回单个元素。但是 getElementsByTagName 返回的是元素的集合,所以里面有 "s"

它返回的是一个集合,不是一个元素!

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

// 行不通
document.getElementsByTagName('input').value = 5;

这是行不通的,因为它需要的是一个 input 的 集合,并将值赋(assign)给它,而不是赋值给其中的一个元素。

我们应该遍历集合或通过对应的索引来获取元素,然后赋值,如下所示:

// 应该可以运行(如果有 input)
document.getElementsByTagName('input')[0].value = 5;

查找 .article 元素:

<form name="my-form">
  <div class="article">Article</div>
  <div class="long article">Long article</div>
</form>

<script>
  // 按 name 特性查找
  let form = document.getElementsByName('my-form')[0];

  // 在 form 中按 class 查找
  let articles = form.getElementsByClassName('article');
  alert(articles.length); // 2, found two elements with class "article"
</script>

实时的集合

所有的 "getElementsBy*" 方法都会返回一个 实时的(live) 集合。这样的集合始终反映的是文档的当前状态,并且在文档发生更改时会“自动更新”。

在下面的例子中,有两个脚本。

  1. 第一个创建了对 <div> 的集合的引用。截至目前,它的长度是 1
  2. 第二个脚本在浏览器再遇到一个 <div> 时运行,所以它的长度是 2
<div>First div</div>

<script>
  let divs = document.getElementsByTagName('div');
  alert(divs.length); // 1
</script>

<div>Second div</div>

<script>
  alert(divs.length); // 2
</script>

相反,querySelectorAll 返回的是一个 静态的 集合。就像元素的固定数组。

如果我们使用它,那么两个脚本都会输出 1

<div>First div</div>

<script>
  let divs = document.querySelectorAll('div');
  alert(divs.length); // 1
</script>

<div>Second div</div>

<script>
  alert(divs.length); // 1
</script>

现在我们可以很容易地看到不同之处。在文档中出现新的 div 后,静态集合并没有增加。

总结

有 6 种主要的方法,可以在 DOM 中搜素节点:

Method Searches by... Can call on an element? Live?
querySelector CSS-selector -
querySelectorAll CSS-selector -
getElementById id - -
getElementsByName name -
getElementsByTagName tag or '*'
getElementsByClassName class

目前为止,最常用的是 querySelectorquerySelectorAll,但是 getElementBy* 可能会偶尔有用,或者可以在旧脚本中找到。

此外:

  • elem.matches(css) 用于检查 elem 与给定的 CSS 选择器是否匹配。
  • elem.closest(css) 用于查找与给定 CSS 选择器相匹配的最近的祖先。elem 本身也会被检查。

让我们在这里提一下另一种用来检查子级与父级之间关系的方法,因为它有时很有用:

  • 如果 elemBelemA 内(elemA 的后代)或者 elemA==elemBelemA.contains(elemB) 将返回 true。

任务

重要程度: 4

这是带有表格(table)和表单(form)的文档。

如何查找?……

  1. 带有 id="age-table" 的表格。
  2. 表格内的所有 label 元素(应该有三个)。
  3. 表格中的第一个 td(带有 “Age” 字段)。
  4. 带有 name="search"form
  5. 表单中的第一个 input
  6. 表单中的最后一个 input

在一个单独的窗口中打开 table.html 页面,并对此页面使用浏览器开发者工具。

实现的方式有很多种。

以下列举的是其中一些方法:

// 1. 带有 id="age-table" 的表格。
let table = document.getElementById('age-table')

// 2. 表格内的所有 label 元素
table.getElementsByTagName('label')
// 或
document.querySelectorAll('#age-table label')

// 3. 表格中的第一个 td(带有 "Age" 字段)
table.rows[0].cells[0]
// 或
table.getElementsByTagName('td')[0]
// 或
table.querySelector('td')

// 4. 带有 name="search" 的 form。
// 假设文档中只有一个 name="search" 的元素
let form = document.getElementsByName('search')[0]
// 或者,专门对于 form
document.querySelector('form[name="search"]')

// 5. 表单中的第一个 input
form.getElementsByTagName('input')[0]
// 或
form.querySelector('input')

// 6. 表单中的最后一个 input
let inputs = form.querySelectorAll('input') // 查找所有 input
inputs[inputs.length-1] // 取出最后一个
教程路线图

评论

在评论之前先阅读本内容…
  • 如果你发现教程有错误,或者有其他需要修改和提升的地方 — 请 提交一个 GitHub issue 或 pull request,而不是在这评论。
  • 如果你对教程的内容有不理解的地方 — 请详细说明。
  • 使用 <code> 标签插入只有几个词的代码,插入多行代码可以使用 <pre> 标签,对于超过 10 行的代码,建议你使用沙箱(plnkrJSBincodepen…)