2022 年 8 月 5 日

節點屬性:類型、標籤和內容

讓我們更深入了解 DOM 節點。

在本章中,我們將更深入了解它們的內容,並學習它們最常用的屬性。

DOM 節點類別

不同的 DOM 節點可能具有不同的屬性。例如,對應於標籤 <a> 的元素節點具有連結相關屬性,而對應於 <input> 的元素節點具有輸入相關屬性,依此類推。文字節點與元素節點不同。但它們之間也有共同的屬性和方法,因為所有類別的 DOM 節點都形成單一層級。

每個 DOM 節點都屬於對應的內建類別。

層級的根源是 EventTarget,它由 Node 繼承,而其他 DOM 節點則從它繼承。

以下是圖片,說明如下

類別為

  • EventTarget – 是所有內容的根部「抽象」類別。

    該類別的物件從未被建立。它用作基礎,讓所有 DOM 節點支援所謂的「事件」,我們稍後會研究它們。

  • Node – 也是一個「抽象」類別,用作 DOM 節點的基礎。

    它提供核心樹狀功能:parentNodenextSiblingchildNodes 等(它們是 getter)。Node 類別的物件從未被建立。但有其他類別從它繼承(因此繼承了 Node 功能)。

  • Document,基於歷史原因,通常由 HTMLDocument 繼承(儘管最新的規範並未規定)– 是文件整體。

    document 全域物件恰好屬於此類別。它用作 DOM 的進入點。

  • CharacterData – 一個「抽象」類別,由下列項目繼承

    • Text – 對應於元素內文字的類別,例如 <p>Hello</p> 中的 Hello
    • Comment – 註解類別。它們不會顯示,但每個註解都會成為 DOM 的成員。
  • Element – 是 DOM 元素的基礎類別。

    它提供元素層級導覽,例如 nextElementSiblingchildren,以及搜尋方法,例如 getElementsByTagNamequerySelector

    瀏覽器不僅支援 HTML,還支援 XML 和 SVG。因此,Element 類別用作更具體類別的基礎:SVGElementXMLElement(我們在此不需要它們)和 HTMLElement

  • 最後,HTMLElement 是所有 HTML 元素的基本類別。我們大部分時間都會使用它。

    它由具體的 HTML 元素繼承

還有許多其他標籤有自己的類別,可能具有特定屬性和方法,而某些元素,例如 <span><section><article> 沒有任何特定屬性,因此它們是 HTMLElement 類別的實例。

因此,給定節點的完整屬性和方法集來自繼承鏈。

例如,我們來考慮 <input> 元素的 DOM 物件。它屬於 HTMLInputElement 類別。

它取得屬性和方法,作為(以繼承順序列出)的疊加

  • HTMLInputElement – 此類別提供輸入特定屬性,
  • HTMLElement – 它提供常見的 HTML 元素方法(以及 getter/setter),
  • Element – 提供一般元素方法,
  • Node – 提供常見的 DOM 節點屬性,
  • EventTarget – 提供對事件(待涵蓋)的支援,
  • …最後它繼承自 Object,因此「一般物件」方法(例如 hasOwnProperty)也可用。

若要查看 DOM 節點類別名稱,我們可以回想物件通常具有 constructor 屬性。它參照類別建構函式,而 constructor.name 是它的名稱

alert( document.body.constructor.name ); // HTMLBodyElement

…或者我們可以只對它進行 toString

alert( document.body ); // [object HTMLBodyElement]

我們也可以使用 instanceof 來檢查繼承

alert( document.body instanceof HTMLBodyElement ); // true
alert( document.body instanceof HTMLElement ); // true
alert( document.body instanceof Element ); // true
alert( document.body instanceof Node ); // true
alert( document.body instanceof EventTarget ); // true

正如我們所見,DOM 節點是一般的 JavaScript 物件。它們使用基於原型的類別進行繼承。

這也很容易透過在瀏覽器中使用 console.dir(elem) 輸出元素來查看。在主控台中,您可以看到 HTMLElement.prototypeElement.prototype 等。

console.dir(elem)console.log(elem)

大多數瀏覽器在其開發人員工具中支援兩個指令:console.logconsole.dir。它們會將其引數輸出到主控台。對於 JavaScript 物件,這些指令通常會執行相同動作。

但對於 DOM 元素,它們是不同的

  • console.log(elem) 顯示元素 DOM 樹。
  • console.dir(elem) 將元素顯示為 DOM 物件,很適合用來探索其屬性。

document.body 上試試看。

規格中的 IDL

在規格中,DOM 類別並非使用 JavaScript 描述,而是使用一種特殊的 介面描述語言 (IDL),通常很容易理解。

在 IDL 中,所有屬性都以其類型為前綴。例如,DOMStringboolean 等。

以下是摘錄,並附有註解

// Define HTMLInputElement
// The colon ":" means that HTMLInputElement inherits from HTMLElement
interface HTMLInputElement: HTMLElement {
  // here go properties and methods of <input> elements

  // "DOMString" means that the value of a property is a string
  attribute DOMString accept;
  attribute DOMString alt;
  attribute DOMString autocomplete;
  attribute DOMString value;

  // boolean value property (true/false)
  attribute boolean autofocus;
  ...
  // now the method: "void" means that the method returns no value
  void select();
  ...
}

「nodeType」屬性

nodeType 屬性提供另一種「老式」方法來取得 DOM 節點的「類型」。

它有一個數字值

  • 元素節點的 elem.nodeType == 1
  • 文字節點的 elem.nodeType == 3
  • 文件物件的 elem.nodeType == 9
  • 規格 中還有其他幾個值。

例如

<body>
  <script>
  let elem = document.body;

  // let's examine: what type of node is in elem?
  alert(elem.nodeType); // 1 => element

  // and its first child is...
  alert(elem.firstChild.nodeType); // 3 => text

  // for the document object, the type is 9
  alert( document.nodeType ); // 9
  </script>
</body>

在現代腳本中,我們可以使用 instanceof 和其他基於類別的測試來查看節點類型,但有時 nodeType 可能比較簡單。我們只能讀取 nodeType,不能變更它。

標籤:nodeName 和 tagName

給定一個 DOM 節點,我們可以從 nodeNametagName 屬性讀取其標籤名稱

例如

alert( document.body.nodeName ); // BODY
alert( document.body.tagName ); // BODY

tagNamenodeName 有什麼不同?

當然,差異反映在它們的名稱中,但確實有點微妙。

  • tagName 屬性僅存在於 Element 節點中。
  • nodeName 定義於任何 Node
    • 對於元素,它的意思與 tagName 相同。
    • 對於其他節點類型(文字、註解等),它有一個包含節點類型的字串。

換句話說,tagName 僅受元素節點支援(因為它源自 Element 類別),而 nodeName 可以說明其他節點類型。

例如,讓我們比較 document 和註解節點的 tagNamenodeName

<body><!-- comment -->

  <script>
    // for comment
    alert( document.body.firstChild.tagName ); // undefined (not an element)
    alert( document.body.firstChild.nodeName ); // #comment

    // for document
    alert( document.tagName ); // undefined (not an element)
    alert( document.nodeName ); // #document
  </script>
</body>

如果我們只處理元素,那麼我們可以使用 tagNamenodeName,沒有差別。

標籤名稱總是使用大寫,除非在 XML 模式中

瀏覽器有兩種處理文件模式:HTML 和 XML。通常,HTML 模式用於網頁。當瀏覽器收到標頭為 Content-Type: application/xml+xhtml 的 XML 文件時,會啟用 XML 模式。

在 HTML 模式中,tagName/nodeName 始終使用大寫:對於 <body><BoDy>,都是 BODY

在 XML 模式中,大小寫會「保持原樣」。現今 XML 模式已鮮少使用。

innerHTML:內容

innerHTML 屬性允許取得元素內的 HTML,並以字串呈現。

我們也可以修改它。因此,這是變更網頁最有效的方法之一。

範例顯示 document.body 的內容,然後將其完全取代

<body>
  <p>A paragraph</p>
  <div>A div</div>

  <script>
    alert( document.body.innerHTML ); // read the current contents
    document.body.innerHTML = 'The new BODY!'; // replace it
  </script>

</body>

我們可以嘗試插入無效的 HTML,瀏覽器會修正我們的錯誤

<body>

  <script>
    document.body.innerHTML = '<b>test'; // forgot to close the tag
    alert( document.body.innerHTML ); // <b>test</b> (fixed)
  </script>

</body>
指令碼不會執行

如果 innerHTML<script> 標籤插入文件,它會成為 HTML 的一部分,但不會執行。

注意:「innerHTML+=」會進行完整覆寫

我們可以使用 elem.innerHTML+="more html" 將 HTML 附加至元素。

像這樣

chatDiv.innerHTML += "<div>Hello<img src='smile.gif'/> !</div>";
chatDiv.innerHTML += "How goes?";

但我們在執行時應非常小心,因為進行的動作並非新增,而是完整覆寫。

技術上來說,這兩行指令執行相同的動作

elem.innerHTML += "...";
// is a shorter way to write:
elem.innerHTML = elem.innerHTML + "..."

換句話說,innerHTML+= 執行下列動作

  1. 移除舊內容。
  2. 改寫新的 innerHTML(舊內容和新內容的串接)。

由於內容會「歸零」並從頭開始改寫,所有影像和其他資源都會重新載入.

在上述的 chatDiv 範例中,指令行 chatDiv.innerHTML+="How goes?" 會重新建立 HTML 內容,並重新載入 smile.gif(希望已快取)。如果 chatDiv 有許多其他文字和影像,則重新載入的動作會非常明顯。

還有其他副作用。例如,如果現有的文字已使用滑鼠選取,大多數瀏覽器會在改寫 innerHTML 時移除選取。如果有一個 <input> 含有訪客輸入的文字,則文字會被移除。以此類推。

幸運的是,除了 innerHTML 之外,還有其他方式可以新增 HTML,我們將很快地研究這些方式。

outerHTML:元素的完整 HTML

outerHTML 屬性包含元素的完整 HTML。這就像 innerHTML 加上元素本身。

以下是一個範例

<div id="elem">Hello <b>World</b></div>

<script>
  alert(elem.outerHTML); // <div id="elem">Hello <b>World</b></div>
</script>

注意:與 innerHTML 不同,寫入 outerHTML 不會變更元素。它會在 DOM 中取代元素。

是的,聽起來很奇怪,而且的確很奇怪,這就是我們在此特別註明的原因。請看。

考慮以下範例

<div>Hello, world!</div>

<script>
  let div = document.querySelector('div');

  // replace div.outerHTML with <p>...</p>
  div.outerHTML = '<p>A new element</p>'; // (*)

  // Wow! 'div' is still the same!
  alert(div.outerHTML); // <div>Hello, world!</div> (**)
</script>

看起來真的很奇怪,對吧?

在指令行 (*) 中,我們將 div 取代為 <p>A new element</p>。在外層文件(DOM)中,我們可以看到新內容,而非 <div>。但是,正如我們在指令行 (**) 中所見,舊 div 變數的值並未變更!

outerHTML 指派不會修改 DOM 元素(在本例中,由變數「div」參照的物件),而是將其從 DOM 中移除,並將新的 HTML 插入其位置。

因此,div.outerHTML=... 中發生的情況是

  • div 已從文件中移除。
  • 另一段 HTML <p>新的元素</p> 已插入其位置。
  • div 仍具有其舊值。新的 HTML 未儲存至任何變數。

在此很容易產生錯誤:修改 div.outerHTML,然後繼續使用 div,就像它包含新內容一樣。但事實並非如此。這對 innerHTML 來說是正確的,但對 outerHTML 來說卻不是。

我們可以寫入 elem.outerHTML,但應記住它不會變更我們寫入的元素(「elem」)。它會將新的 HTML 放入其位置。我們可以透過查詢 DOM 來取得對新元素的參考。

nodeValue/data:文字節點內容

innerHTML 屬性僅對元素節點有效。

其他節點類型,例如文字節點,有其對應的 nodeValuedata 屬性。這兩個屬性在實際使用上幾乎相同,只有細微的規格差異。因此我們將使用 data,因為它較短。

讀取文字節點和註解內容的範例

<body>
  Hello
  <!-- Comment -->
  <script>
    let text = document.body.firstChild;
    alert(text.data); // Hello

    let comment = text.nextSibling;
    alert(comment.data); // Comment
  </script>
</body>

對於文字節點,我們可以想像出讀取或修改它們的理由,但為什麼是註解?

有時開發人員會將資訊或範本指示嵌入 HTML 中,如下所示

<!-- if isAdmin -->
  <div>Welcome, Admin!</div>
<!-- /if -->

…然後 JavaScript 可以從 data 屬性讀取它並處理嵌入式指示。

textContent:純文字

textContent 提供對元素內部文字的存取:只有文字,減去所有 <標籤>

例如

<div id="news">
  <h1>Headline!</h1>
  <p>Martians attack people!</p>
</div>

<script>
  // Headline! Martians attack people!
  alert(news.textContent);
</script>

如我們所見,只會傳回文字,就像所有 <標籤> 都被剪掉,但其中的文字仍保留。

在實務上,很少需要讀取此類文字。

寫入 textContent 更為有用,因為它允許以「安全的方式」寫入文字。

假設我們有一個任意字串,例如由使用者輸入,並想要顯示它。

  • 使用 innerHTML 時,它會「以 HTML 方式」插入,包含所有 HTML 標籤。
  • 使用 textContent 時,它會「以文字方式」插入,所有符號都以字面意義處理。

比較這兩者

<div id="elem1"></div>
<div id="elem2"></div>

<script>
  let name = prompt("What's your name?", "<b>Winnie-the-Pooh!</b>");

  elem1.innerHTML = name;
  elem2.textContent = name;
</script>
  1. 第一個 <div> 取得名稱「以 HTML 方式」:所有標籤都變成標籤,因此我們會看到粗體名稱。
  2. 第二個 <div> 取得名稱「以文字方式」,因此我們會看到字面上的 <b>Winnie-the-Pooh!</b>

在大多數情況下,我們預期從使用者取得文字,並希望將其視為文字。我們不希望我們的網站出現意外的 HTML。指定給 textContent 的內容會執行此動作。

「hidden」屬性

「hidden」屬性和 DOM 屬性會指定元素是否可見。

我們可以在 HTML 中使用它,或使用 JavaScript 指定它,如下所示

<div>Both divs below are hidden</div>

<div hidden>With the attribute "hidden"</div>

<div id="elem">JavaScript assigned the property "hidden"</div>

<script>
  elem.hidden = true;
</script>

技術上來說,hidden 的運作方式與 style="display:none" 相同。但寫起來比較簡短。

以下是一個閃爍的元素

<div id="elem">A blinking element</div>

<script>
  setInterval(() => elem.hidden = !elem.hidden, 1000);
</script>

更多屬性

DOM 元素也有其他屬性,特別是那些取決於類別的屬性

  • value<input><select><textarea> (HTMLInputElementHTMLSelectElement…) 的值。
  • href<a href="..."> (HTMLAnchorElement) 的「href」。
  • id – 所有元素 (HTMLElement) 的「id」屬性的值。
  • …還有更多…

例如

<input type="text" id="elem" value="value">

<script>
  alert(elem.type); // "text"
  alert(elem.id); // "elem"
  alert(elem.value); // value
</script>

大多數標準 HTML 屬性都有對應的 DOM 屬性,我們可以像這樣存取它。

如果我們想要知道給定類別支援的完整屬性清單,我們可以在規範中找到它們。例如,HTMLInputElement 記錄在 https://html.spec.whatwg.org/#htmlinputelement

或者,如果我們想要快速取得它們,或有興趣取得具體的瀏覽器規範,我們可以隨時使用 console.dir(elem) 輸出元素並讀取屬性。或者在瀏覽器開發人員工具的「元素」標籤中探索「DOM 屬性」。

摘要

每個 DOM 節點都屬於某個類別。這些類別形成一個階層。完整的屬性和方法集是繼承的結果。

主要的 DOM 節點屬性為

nodeType
我們可以使用它來查看節點是文字節點還是元素節點。它有一個數字值:元素為 1,文字節點為3,其他節點類型則為其他數字。唯讀。
nodeName/tagName
對於元素,標籤名稱(除非處於 XML 模式,否則會轉換為大寫)。對於非元素節點,nodeName 會描述它是什麼。唯讀。
innerHTML
元素的 HTML 內容。可以修改。
outerHTML
元素的完整 HTML。寫入 elem.outerHTML 的操作不會觸及 elem 本身。相反地,它會在外部內容中被新的 HTML 取代。
nodeValue/data
非元素節點(文字、註解)的內容。這兩個幾乎相同,通常我們使用 data。可以修改。
textContent
元素內部的文字:HTML 減去所有 <標籤>。寫入其中會將文字放入元素內部,所有特殊字元和標籤都將被視為文字。可以安全地插入使用者產生的文字,並防止不必要的 HTML 插入。
hidden
設定為 true 時,與 CSS display:none 相同。

DOM 節點也有其他屬性,具體取決於它們的類別。例如,<input> 元素(HTMLInputElement)支援 valuetype,而 <a> 元素(HTMLAnchorElement)支援 href 等。大多數標準 HTML 屬性都有對應的 DOM 屬性。

但是,HTML 屬性和 DOM 屬性並不總是相同的,正如我們在下一章中所看到的。

任務

重要性:5

有一個樹狀結構,以巢狀 ul/li 呈現。

撰寫程式碼,針對每個 <li> 顯示

  1. 內部的文字(不含子樹)
  2. 巢狀 <li> 的數量 - 所有後代,包括深度巢狀的後代。

在新視窗中示範

為任務開啟沙盒。

讓我們對 <li> 進行迴圈。

for (let li of document.querySelectorAll('li')) {
  ...
}

在迴圈中,我們需要取得每個 li 內部的文字。

我們可以從 li 的第一個子節點(文字節點)讀取文字

for (let li of document.querySelectorAll('li')) {
  let title = li.firstChild.data;

  // title is the text in <li> before any other nodes
}

然後,我們可以取得後代的數量,方法是 li.getElementsByTagName('li').length

在沙盒中開啟解決方案。

重要性:5

腳本顯示什麼?

<html>

<body>
  <script>
    alert(document.body.lastChild.nodeType);
  </script>
</body>

</html>

這裡有一個陷阱。

<script> 執行時,最後一個 DOM 節點恰好是 <script>,因為瀏覽器尚未處理頁面的其餘部分。

因此,結果為 1(元素節點)。

<html>

<body>
  <script>
    alert(document.body.lastChild.nodeType);
  </script>
</body>

</html>
重要性:3

此程式碼顯示什麼?

<script>
  let body = document.body;

  body.innerHTML = "<!--" + body.tagName + "-->";

  alert( body.firstChild.data ); // what's here?
</script>

答案:BODY

<script>
  let body = document.body;

  body.innerHTML = "<!--" + body.tagName + "-->";

  alert( body.firstChild.data ); // BODY
</script>

逐步說明

  1. <body> 的內容會被註解取代。註解為 <!--BODY-->,因為 body.tagName == "BODY"。如我們所知,HTML 中的 tagName 始終為大寫。
  2. 註解現在是唯一的子節點,因此我們可以在 body.firstChild 中取得它。
  3. 註解的 data 屬性為其內容(在 <!--...--> 內):"BODY"
重要性:4

document 屬於哪個類別?

它在 DOM 階層結構中的位置在哪裡?

它繼承自 NodeElement 還是 HTMLElement

我們可以透過輸出它來查看它屬於哪個類別,例如

alert(document); // [object HTMLDocument]

alert(document.constructor.name); // HTMLDocument

因此,documentHTMLDocument 類別的一個實例。

它在階層結構中的位置在哪裡?

是的,我們可以瀏覽規格,但手動找出會更快。

讓我們透過 __proto__ 來遍歷原型鏈。

如我們所知,類別的方法在建構函式的 prototype 中。例如,HTMLDocument.prototype 有文件的方法。

此外,prototype 內部有一個指向建構函式函式的參考

alert(HTMLDocument.prototype.constructor === HTMLDocument); // true

若要取得類別名稱為字串,我們可以使用 constructor.name。讓我們對整個 document 原型鏈執行此操作,直到類別 Node

alert(HTMLDocument.prototype.constructor.name); // HTMLDocument
alert(HTMLDocument.prototype.__proto__.constructor.name); // Document
alert(HTMLDocument.prototype.__proto__.__proto__.constructor.name); // Node

這就是階層結構。

我們也可以使用 console.dir(document) 來檢查物件,並透過開啟 __proto__ 來查看這些名稱。主控台會從內部將它們取自 constructor

教學課程地圖

註解

在評論之前先閱讀這段話…
  • 如果您有改進建議,請 提交 GitHub 問題 或發起プル要求,而不是發表評論。
  • 如果您無法理解文章中的某些內容,請詳細說明。
  • 要插入少數幾個字的程式碼,請使用 <code> 標籤,若要插入多行,請將它們包在 <pre> 標籤中,若要插入超過 10 行,請使用沙盒 (plnkrjsbincodepen…)