2021 年 12 月 12 日

瀏覽 DOM

DOM 允許我們對元素及其內容執行任何操作,但我們首先需要找到對應的 DOM 物件。

所有 DOM 上的操作都從 document 物件開始。那是 DOM 的主要「進入點」。我們可以從中存取任何節點。

以下是允許在 DOM 節點之間移動的連結圖片

讓我們更詳細地討論它們。

最上方:documentElement和主體

最上層的樹狀節點可直接用 document 屬性取得

<html> = document.documentElement
最上層的文件節點為 document.documentElement。這是 <html> 標籤的 DOM 節點。
<body> = document.body
另一個廣泛使用的 DOM 節點是 <body> 元素 – document.body
<head> = document.head
<head> 標籤可用於 document.head
有一個陷阱:document.body 可能為 null

腳本無法存取在執行時不存在的元素。

特別是,如果腳本在 <head> 內部,則 document.body 不可使用,因為瀏覽器尚未讀取它。

因此,在以下範例中,第一個 alert 會顯示 null

<html>

<head>
  <script>
    alert( "From HEAD: " + document.body ); // null, there's no <body> yet
  </script>
</head>

<body>

  <script>
    alert( "From BODY: " + document.body ); // HTMLBodyElement, now it exists
  </script>

</body>
</html>
在 DOM 世界中,null 表示「不存在」

在 DOM 中,null 值表示「不存在」或「沒有此節點」。

子節點:childNodes、firstChild、lastChild

從現在開始,我們將使用兩個術語

  • 子節點(或子節點) – 直接的子元素。換句話說,它們正好巢狀在給定的元素中。例如,<head><body><html> 元素的子節點。
  • 後代 – 巢狀在給定元素中的所有元素,包括子節點、其子節點,依此類推。

例如,這裡 <body> 有子節點 <div><ul>(以及一些空白文字節點)

<html>
<body>
  <div>Begin</div>

  <ul>
    <li>
      <b>Information</b>
    </li>
  </ul>
</body>
</html>

…而 <body> 的後代不僅是直接子節點 <div><ul>,還包括更深層巢狀的元素,例如 <li><ul> 的子節點)和 <b><li> 的子節點)– 整個子樹。

childNodes 集合列出所有子節點,包括文字節點。

以下範例顯示 document.body 的子節點

<html>
<body>
  <div>Begin</div>

  <ul>
    <li>Information</li>
  </ul>

  <div>End</div>

  <script>
    for (let i = 0; i < document.body.childNodes.length; i++) {
      alert( document.body.childNodes[i] ); // Text, DIV, Text, UL, ..., SCRIPT
    }
  </script>
  ...more stuff...
</body>
</html>

請注意這裡一個有趣的細節。如果我們執行上述範例,顯示的最後一個元素是 <script>。事實上,文件下方還有更多內容,但在執行腳本時,瀏覽器尚未讀取它,因此腳本看不到它。

屬性 firstChildlastChild 提供快速存取第一個和最後一個子節點。

它們只是簡寫。如果存在子節點,則以下內容始終為真

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

還有一個特殊函數 elem.hasChildNodes() 用於檢查是否有任何子節點。

DOM 集合

正如我們所見,childNodes 看起來像一個陣列。但實際上它不是陣列,而是一個集合 - 一個特殊的類陣列可迭代物件。

這有兩個重要的後果

  1. 我們可以使用 for..of 來迭代它
for (let node of document.body.childNodes) {
  alert(node); // shows all nodes from the collection
}

這是因為它是可迭代的(提供 Symbol.iterator 屬性,如要求)。

  1. 陣列方法不起作用,因為它不是陣列
alert(document.body.childNodes.filter); // undefined (there's no filter method!)

第一件事很好。第二件事是可以容忍的,因為如果我們想要陣列方法,我們可以使用 Array.from 從集合中建立一個「真正的」陣列

alert( Array.from(document.body.childNodes).filter ); // function
DOM 集合是唯讀的

DOM 集合,甚至更多 - 本章中列出的所有導覽屬性都是唯讀的。

我們不能透過指定 childNodes[i] = ... 來用其他東西取代子節點。

變更 DOM 需要其他方法。我們將在下一章看到它們。

DOM 集合是動態的

幾乎所有 DOM 集合,除了少數例外,都是動態的。換句話說,它們反映了 DOM 的當前狀態。

如果我們保留對 elem.childNodes 的參照,並在 DOM 中新增/移除節點,則它們會自動出現在集合中。

不要使用 for..in 來迴圈集合

集合使用 for..of 可迭代。有時人們會嘗試使用 for..in 來執行此操作。

請不要這樣做。for..in 迴圈會迭代所有可列舉的屬性。而集合有一些我們通常不想要的「額外」很少使用的屬性

<body>
<script>
  // shows 0, 1, length, item, values and more.
  for (let prop in document.body.childNodes) alert(prop);
</script>
</body>

兄弟節點和父節點

兄弟節點 是同一個父節點的子節點。

例如,這裡的 <head><body> 是兄弟節點

<html>
  <head>...</head><body>...</body>
</html>
  • <body> 被稱為 <head> 的「下一個」或「右邊」兄弟節點,
  • <head> 被稱為 <body> 的「上一個」或「左邊」兄弟節點。

下一個兄弟節點在 nextSibling 屬性中,上一個兄弟節點在 previousSibling 中。

父節點可用作 parentNode

例如

// parent of <body> is <html>
alert( document.body.parentNode === document.documentElement ); // true

// after <head> goes <body>
alert( document.head.nextSibling ); // HTMLBodyElement

// before <body> goes <head>
alert( document.body.previousSibling ); // HTMLHeadElement

僅元素導覽

上面列出的導覽屬性是指所有節點。例如,在 childNodes 中,我們可以看到文字節點、元素節點,甚至如果它們存在的話,還可以看見註解節點。

但對於許多任務,我們不需要文字或註解節點。我們想要操作表示標籤並形成頁面結構的元素節點。

因此,讓我們查看更多僅考慮元素節點的導覽連結

這些連結類似於上面提供的連結,只是裡面有 Element 字詞

  • children – 僅為元素節點的那些子節點。
  • firstElementChildlastElementChild – 第一個和最後一個元素子節點。
  • previousElementSiblingnextElementSibling – 鄰近元素。
  • parentElement – 父元素。
為什麼是 parentElement?父節點不能是元素嗎?

parentElement 屬性傳回「元素」父節點,而 parentNode 傳回「任何節點」父節點。這些屬性通常相同:它們都取得父節點。

除了 document.documentElement 之外

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

原因是根節點 document.documentElement (<html>) 的父節點為 document。但 document 不是元素節點,因此 parentNode 會傳回它,而 parentElement 則不會。

當我們想要從任意元素 elem 往上升到 <html>,但不升到 document 時,這個細節可能很有用

while(elem = elem.parentElement) { // go up till <html>
  alert( elem );
}

讓我們修改上面的一個範例:將 childNodes 替換為 children。現在它只顯示元素

<html>
<body>
  <div>Begin</div>

  <ul>
    <li>Information</li>
  </ul>

  <div>End</div>

  <script>
    for (let elem of document.body.children) {
      alert(elem); // DIV, UL, DIV, SCRIPT
    }
  </script>
  ...
</body>
</html>

更多連結:表格

到目前為止,我們描述了基本的導覽屬性。

某些類型的 DOM 元素可能會提供額外的屬性,這些屬性特定於它們的類型,以方便使用。

表格就是一個很好的範例,而且是一個特別重要的案例

<table> 元素支援(除了上面提供的屬性之外)這些屬性

  • table.rows – 表格中 <tr> 元素的集合。
  • table.caption/tHead/tFoot – 參照元素 <caption><thead><tfoot>
  • table.tBodies<tbody> 元素的集合(根據標準可以有很多個,但至少會有一個 - 即使原始 HTML 中沒有,瀏覽器也會將它放入 DOM 中)。

<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> 內的編號。

使用範例

<table id="table">
  <tr>
    <td>one</td><td>two</td>
  </tr>
  <tr>
    <td>three</td><td>four</td>
  </tr>
</table>

<script>
  // get td with "two" (first row, second column)
  let td = table.rows[0].cells[1];
  td.style.backgroundColor = "red"; // highlight it
</script>

規格:表格資料

HTML 表單也有額外的導覽屬性。我們在開始使用表單時會再探討。

摘要

給定一個 DOM 節點,我們可以使用導覽屬性前往它的鄰近節點。

導覽屬性主要有兩組

  • 所有節點:parentNodechildNodesfirstChildlastChildpreviousSiblingnextSibling
  • 僅限元素節點:parentElementchildrenfirstElementChildlastElementChildpreviousElementSiblingnextElementSibling

某些類型的 DOM 元素(例如表格)提供額外的屬性和集合來存取其內容。

作業

重要性:5

檢視此頁面

<html>
<body>
  <div>Users:</div>
  <ul>
    <li>John</li>
    <li>Pete</li>
  </ul>
</body>
</html>

對於下列各項,請提供至少一種存取方式

  • <div> DOM 節點?
  • <ul> DOM 節點?
  • 第二個 <li>(包含 Pete)?

有很多方法,例如

<div> DOM 節點

document.body.firstElementChild
// or
document.body.children[0]
// or (the first node is space, so we take 2nd)
document.body.childNodes[1]

<ul> DOM 節點

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

第二個 <li>(包含 Pete)

// get <ul>, and then get its last element child
document.body.lastElementChild.lastElementChild
重要性:5

如果 elem – 是任意 DOM 元素節點…

  • elem.lastChild.nextSibling 是否永遠為 null
  • elem.children[0].previousSibling 是否永遠為 null ?
  1. 是的,正確。元素 elem.lastChild 永遠是最後一個,它沒有 nextSibling
  2. 不,錯誤,因為 elem.children[0] 是元素中的第一個子節點。但在它之前可能存在非元素節點。因此 previousSibling 可能是一個文字節點。

請注意:對於這兩種情況,如果沒有子節點,就會發生錯誤。

如果沒有子節點,elem.lastChild 會是 null,所以我們無法存取 elem.lastChild.nextSibling。而且 elem.children 集合是空的(就像一個空的陣列 [])。

重要性:5

撰寫程式碼將所有對角線表格儲存格塗成紅色。

你需要從 <table> 取得所有對角線的 <td>,並使用以下程式碼將它們塗成紅色

// td should be the reference to the table cell
td.style.backgroundColor = 'red';

結果應該是

開啟一個沙盒來執行此任務。

我們將使用 rowscells 屬性來存取對角線表格儲存格。

在沙盒中開啟解答。

教學課程地圖

留言

留言前請先閱讀…
  • 如果你有建議要如何改進 - 請 提交 GitHub 議題 或提出拉取請求,而不是留言。
  • 如果你無法理解文章中的某個部分 - 請說明。
  • 要插入幾行程式碼,請使用 <code> 標籤,要插入多行程式碼,請將它們包覆在 <pre> 標籤中,要插入超過 10 行程式碼,請使用沙盒 (plnkrjsbincodepen…)