三、遍历 DOM

对 DOM 的所有操作都是以document对象开始的。从它我们可以访问任何节点。

下图是描述对象间关系的图片,通过这些关系我们可以在 DOM 节点之间切换

image-20211228212806935

3.1 document.documentElement 和 Body

最顶层的树节点可以直接作为 document 对象的属性来使用:

  • <html>=document.documentElement 顶层 DOM 节点
  • <body>=document.body
  • <head>=document.head

document.body 可以是个 null。

如果我们没写 body 标签,DOM 会自动创建,但是在以下情况下 body 可能是个 null

html
1<html>
2  <head>
3    <script>
4      alert('From HEAD: ' + document.body) // null,这里目前还没有 <body>
5    </script>
6  </head>
7
8  <body>
9    <script>
10      alert('From BODY: ' + document.body) // HTMLBodyElement,现在存在了
11    </script>
12  </body>
13</html>

因为 DOM 节点是从上到下解析并形成的,如果脚本在 head 就开始运行了,浏览器还没读到 body,所以就不存在 document.body。

3.2 子节点:childNodes,firstChild,lastChild

  • 子节点含义:对应的直系子元素。它们被完全被嵌套在给定的元素中。例如head和body标签就是html元素的子元素。
  • 子孙元素含义:嵌套在给定元素的所有元素,包括子元素以及子元素的子元素等。

如以下代码

html
1<html>
2  <body>
3    <div>Begin</div>
4
5    <ul>
6      <li>
7        <b>Information</b>
8      </li>
9    </ul>
10  </body>
11</html>

body 节点的子节点是 div 节点跟 ul 节点(包括一些空白空格的文本节点)

body 的子孙元素是包含 div、ul、li、b 等子孙元素。

查子节点

我们可以用childNodes来列出所有子节点,包括文本节点。

html
1<html>
2  <body>
3    <div>Begin</div>
4
5    <ul>
6      <li>Information</li>
7    </ul>
8
9    <div>End</div>
10
11    <script>
12      for (let i = 0; i < document.body.childNodes.length; i++) {
13        alert(document.body.childNodes[i]) // Text, DIV, Text, UL, ..., SCRIPT
14      }
15    </script>
16    ...more stuff...
17  </body>
18</html>

上面的代码会将 body 的所有子节点都打出来。

但不会打出...more stuff ...因为脚本运行时这个内容还没解析出来,所以浏览器读不到。

查第一个子节点:firstChild

查最后一个子节点:lastChild

查是否有子节点:node.hasChildNodes()

3.3 DOM 集合

childNodes 是一个伪数组,是部署了 iterator 接口的可迭代伪数组。

我们可以用 for..of 迭代它,但无法使用数组的方法。

不过我们可以用一些方法来让它使用数组的方法:

  • Array.from、[...childNodes]变成真数组
  • 用 call、apply 等方法

有几个注意点:

  • DOM 集合是只读的,我们不能用 childNodes[i]=...的操作来替换一个子节点
  • DOM 集合是实时的,它反映 DOM 的当前状态
  • 不要用 for...in 对集合进行迭代,原因是 for..in 会遍历所有可枚举(enumerable)属性。DOM 集合中就有一些我们并不需要的这些属性会被 for..in 迭代到

3.4 兄弟节点和父节点

兄弟(Sibling)节点指的是有同一个父节点的节点。

html
1<html>
2  <head>
3    ...
4  </head>
5  <body>
6    ...
7  </body>
8</html>
  • body 是 head 的下一个兄弟节点

  • head 是 body 的前一个兄弟节点

父节点:parentNode

上一个兄弟节点:previousSibling

下一个兄弟节点:nextSibling

javascript
1console.log(document.body.parentNode.tagName) // HTML
2console.log(document.head.nextSibling.tagName) // BODY
3console.log(document.body.previousSibling.tagName) // HEAD

3.5 纯元素导航

childNodes 中有很多节点,包括文本节点、元素节点、注释节点等。

但是有时候我们只想要元素节点。

以下就是纯元素节点的导航

image-20211229153355775

这些纯元素节点跟上面的节点访问类似,区别在于需要添加Element

  • Children —— 纯元素节点的子节点
  • firstElementChild —— 第一个子元素节点
  • lastElementChild —— 最后一个子元素节点
  • previousElementSibling —— 上一个兄弟元素节点
  • nextElementSibling —— 下一个兄弟元素节点
  • parentElement —— 父元素节点

parentElement 和 parentNode 的区别

从语义上来说 parentElement 是找父元素节点而 parentNode 是找父节点。

一般来说这两个是一样的,都是用于获取父节点。

不过出现这个 API 可能是由于document.documentElement的parentElement和parentNode不一致导致的

javascript
1document.documentElement.parentNode // document
2document.documentElement.parentElement // null

因为 html 的父节点就是 document 对象

但是 document 并不是元素节点。

当我们希望从下到上遍历到 html 时,可能这个细节有用

javascript
1while(elem === elem.parentElement){ //向上遍历,直到html顶层
2	...
3}

3.6 特定的 DOM 属性

某些类型的 DOM 元素可能会提供特定于类型的其他属性。

比如表格(Table)

<Table>除了支持上面的基本属性,还额外支持以下属性:

  • table.rows - <tr>元素的集合
  • table.caption/tHead/tFoot - 引用表格的<caption>``<thead>``<tfoot>
  • table.tBodies - <tbody>元素的集合

<thead>,<tfoot>,<tbody> 元素提供了 rows 属性:

  • tbody.rows — 表格内部 <tr> 元素的集合。

<tr>:

  • tr.cells — 在给定 <tr> 中的 <td> 和 <th> 单元格的集合。
  • tr.sectionRowIndex — 给定的 <tr> 在封闭的 <thead>/<tbody>/<tfoot> 中的位置(索引)。
  • tr.rowIndex — 在整个表格中 <tr> 的编号(包括表格的所有行)。

<td> 和 <th>:

  • td.cellIndex — 在封闭的 <tr> 中单元格的编号。

3.7 小结

给定一个 DOM 节点,我们可以使用导航(navigation)属性 访问其直接的邻居

  • 对于所有节点:parentNode、childNodes、firstChild、lastChild、nextSibling、previousSibling
  • 对于所有元素节点:parentElement、children、firstElementChild、lastElementChild、nextElementSibling、previousElementSibling
  • 有一些特定的元素还能有额外的属性