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

DOM 允许对元素及其内容进行任何操作,但首先我们需要获得相应的 DOM 对象,将其存入一个变量,然后我们才能对其进行修改。

所有的 DOM 操作起始于 document 对象,使用它可以获得文档中的任何节点。

下面这张图片展示了在 DOM 节点之间遍历的方法:

遍历 DOM - 图1

下面针对图片里的内容,进行详细说明。

最顶部:documentElement 和 body

document 对象上可以直接获取的节点元素有:

** = document.documentElement**

最顶部的节点是 document.documentElement,对应 <html> 标签。

** = document.body**

节点 document.body,对应 <body> 标签。

** = document.head**

节点 document.head,对应 <head> 标签

遍历 DOM - 图2docuemnt.body** 的结果可能是 ****null**

脚本执行时,只能访问到脚本运行处之前的 DOM 节点,如果访问了之后的节点,返回结果就为 null

举个例子:我们在 <head> 标签中书写代码,获取 document.body 时,就会得到 null

  1. <html>
  2. <head>
  3. <script>
  4. alert( '在 <head> 中: ' + document.body ); // null。还没到能访问 <body> 的时候
  5. </script>
  6. </head>
  7. <body>
  8. <script>
  9. alert( '在 <body> 中: ' + document.body ); // HTMLBodyElement。现在有了
  10. </script>
  11. </body>
  12. </html>

遍历 DOM - 图3在 DOM 世界,null 表示“不存在”

在 DOM 中,null`` 意味着“不存在”或者“没有这个节点”。

孩子:childNodes,firstChild 和 lastChild

从现在开始,我们将使用两种术语:

  • 孩子节点(或叫孩子):指元素的直接子元素。比如,<head><body><html> 元素的孩子节点。

  • 后代:元素中嵌套的所有后代元素,包括孩子、孩子的孩子……

例如,这里的 <body> 包含孩子 <div><ul>(还有一些空文本节点):

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

如果我们要查找 <body> 的所有后代元素,除了 <div><ul><ul> 的中的嵌套元素像 <li><ul> 的孩子)和 <b><li> 的孩子)都包含在内。

childNodes 获取的元素集合,是某元素下的所有孩子节点,包括文本节点。

下面例子中展示了 document.body 下的孩子节点:

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

值得我们注意的有趣一点是,执行上面的例子,最后一个元素显示是 <script>。实际上,<script> 下面还有内容呢,但是在脚本执行的这一刻,下面的节点内容还没有读到呢,也就是说 <script> 后面的内容对它是不可见的。

firstChildlastChild 属性让我们能够快速访问某个元素第一个和最后一个孩子节点。

下例中,如果子节点存在,等式总是成立:

  1. elem.childNodes[0] === elem.firstChild
  2. elem.childNodes[elem.childNodes.length - 1] === elem.lastChild

还有一个 elem.hasChildNodes() 方法,用来判断某个元素下是否包含任何子节点。

DOM 集合

childNodes 看起来像个数组,但并不是数组,而是一个集合——一个特殊的是类数组可迭代对象。

有两个重要的结果:

  1. 我们可以用 for..of 去遍历这个集合:
  1. for (let node of document.body.childNodes) {
  2. alert(node); // 展示集合中的所有节点
  3. }

因为这个集合是可迭代的(具有必要的 Symbol.iterator 属性)。

  1. 数组方法不能在集合中使用,因为它不是数组!
  1. alert(document.body.childNodes.filter); // undefined (就没有 filter 方法!)

没有办法了吗?有,将集合转化成数组就可以了,用 ES6 提供的 Array.from 方法就能做到。

  1. alert(Array.from(document.body.childNodes).filter);

遍历 DOM - 图4DOM 集合是只读的

在本章中列举的所有遍历属性都是只读的。

我们不能用 childNodes[i] = ..`` 的方式创建一个节点。

修改 DOM 需要其他方法,会在下一章学习。

遍历 DOM - 图5DOM 集合是实时的

除了少数例外,几乎所有DOM集合都是实时的。换句话说,它们反映了 DOM 的当前状态。

如果我们用一个变量引用了 childNodes``,并做了一些添加/删除操作,变化会在这个变量上“自动更新”。

遍历 DOM - 图6不要在集合上用 **for..in`` 循环**

受到 for..of 可以遍历集合的诱导,有些人认为也可以用 for..in 遍历集合。

请不要!因为 for..in 循环本质上是遍历出对象上的所有可枚举属性,集合对象身上也存在少量很少使用的“其他”属性,会被我们遍历出来的。

  1. <body>
  2. <script>
  3. // 打印出 0, 1, length, item, values 等等.
  4. for (let prop in document.body.childNodes) alert(prop);
  5. </script>
  6. </body>

兄弟节点和父节点

兄弟节点是指拥有同一个父节点的节点。比如,<head><body> 就是兄弟节点:

  • <body> 可以说成在 <head> 的“后面”或者“右面”,

  • <head> 可以说成在 <body> 的“前面”或者“左面”。

父节点使用 parentNode 属性获得。

某个节点的下一个节点,使用 nextSibling 获得;上一个节点使用 previousSibling 获得。

举个例子:

  1. <html><head></head><body><script>
  2. // 稠密排列的 HTML 文档保证规避额外的"空白"文本节点.
  3. // <body> 的父节点是 <html>
  4. alert( document.body.parentNode === document.documentElement ); // true
  5. // <head> 的下一个节点是 <body>
  6. alert( document.head.nextSibling ); // HTMLBodyElement
  7. // <body> 的上一个节点是 <head>
  8. alert( document.body.previousSibling ); // HTMLHeadElement
  9. </script></body></html>

纯元素遍历

使用 childNodes 得到的子元素集合中,包含文本、元素,还有注释节点(有的话)。

但许多时候,我们不需要文本和注释节点,我们想要操作的是页面中的元素节点。

下面这张图片反应了 DOM 中,元素节点之间的遍历关系:

遍历 DOM - 图7

childNodes 类似,不过这里只考虑了元素节点的情况:

  • children:所有的孩子节点(元素)。

  • firstElementChildlastElementChild:第一个、最后一个孩子节点(元素类型)。

  • previousElementSiblingnextElementSibling:兄弟节点(元素类型)。

  • parentElement:父节点(元素类型)。

遍历 DOM - 图8为什么有 parentElement,难道父级还能不是元素?

是的,可能。我们知道,parentElement返回“元素”节点,而 parentNode 可返回“任何类型”的父节点。

它们基本是一样的,但有一个区别,就是在 document.documentElement`` 上:

  1. alert( document.documentElement.parentNode ); // document
  2. alert( document.documentElement.parentElement ); // null

documentElement(即 <html>)就是根节点了,docuement是它的 parentNode;但是 document不是一个元素节点,所以 documentElement 的 parentElement就是 null 啦。

有时,我们在遍历父级链时,要针对每一个遍历的父级元素进行修改,并不想去碰 document(因为 document 对象不具备元素节点方法),那么就可以选择使用 parentElement`` 。

更多链接:表格

到现在为止,我们已经讲解了基本的遍历属性。

某些特定 DOM 元素为了便捷,还提供了针对自身类型的额外属性。其中表格就是一个鲜活的例子。

`` 元素支持(除了上面提到的)还支持以下属性:

  • table.rows:表格中所有 <tr> 元素的集合。

  • table.caption/tHead/tFoot:分别指表格的 <caption><thead><tFoot> 元素。

  • table.tBodies<tbody> 元素的集合(根据标准,一个 <table> 可以有多个 <tbody>)。

**、<tfoot>**` 元素也提供了rows` 属性:

  • tbody.rows:当前 <tbody> 中的 <tr> 元素集合。

``:

  • tr.cells:当前 <tr><td>/<th> 单元格的集合。

  • tr.sectionRowIndex:当前 <tr><thead>/<tbody>/<tFoot> 中索引值。

  • tr.rowIndex: 当前 <tr> 在整个表格中的索引值。

** 和 <th>**

  • td.cellIndex:当前 <td> 单元格在所属 <tr> 中的索引值。

用法示例:

  1. <table id="table">
  2. <tr>
  3. <td>one</td><td>two</td>
  4. </tr>
  5. <tr>
  6. <td>three</td><td>four</td>
  7. </tr>
  8. </table>
  9. <script>
  10. // 获得表格第一行数据的第二个单元格里的内容
  11. alert( table.rows[0].cells[1].innerHTML ) // "two"
  12. </script>

规范:tabular data

HTML 表单元素也有其特定的遍历属性,这些知识点会在之后的章节中学习。

总结

给定一个 DOM 节点,我们可以使用遍历属性访问它的邻居节点。

有两个主要的范围:

  • 针对所有类型节点:parentNodechildNodesfirstChildlastChildpreviousSiblingnextSibling

  • 仅针对元素类型节点:parentElementchildrenfirstElementChildlastElementChildpreviousElementSiblingnextElementSibling

对于一些特定类型的 DOM 元素,比如表格,还提供了额外的便捷属性来访问它们的内容。

练习题

问题

一、DOM 的孩子元素节点

有页面

  1. <html>
  2. <body>
  3. <div>Users:</div>
  4. <ul>
  5. <li>John</li>
  6. <li>Pete</li>
  7. </ul>
  8. </body>
  9. </html>

怎么访问:

  1. <div> 节点?

  2. <ul> 节点?

  3. 第二个 <li> 节点?(包含文本 Pete 的)

二、兄弟节点

假设 elem 是任意的元素节点:

  1. 我们说,elem.lastChild.nextSibling 的结果总是 null,是否正确?

  2. 我们说,elem.children[0].previousSibling 的结果总是 null,是否正确?

三、选择对角线上的单元格

将一个表格对角线上的单元格统一设置成红色背景,应该怎么做?

  1. td.style.backgroundColor = 'red';

最终显示效果如下

遍历 DOM - 图9

答案

一、

  1. document.body.firstElementChild / document.body.children[0]

  2. document.body.lastElementChild / document.body.children[1]

  3. document.body.lastElementChild.lastElementChild

二、

  1. 正确。因为 lastChild 总是指元素的最后一个节点(任意类型),后面不再可能有节点了。

  2. 错误。children[0] 仅表示元素 elem 的第一个元素子节点,它的前一个可能还有一个文本节点。

需要注意的是,如果我们访问的 elem 没有孩子的话,那么不论访问 lastChild 还是 children[0] 都是得到 null。如果在 null 上还要进一步访问 nextSiblingpreviousSibling 的话,就会报错了。

三、

主要利用 .rows.cells 属性来实现该效果。核心代码如下:

  1. for (let i = 0; i < table.rows.length; i++) {
  2. let row = table.rows[i];
  3. row.cells[i].style.backgroundColor = 'red';
  4. }

(完)