讓我們更深入了解 DOM 節點。
在本章中,我們將更深入了解它們的內容,並學習它們最常用的屬性。
DOM 節點類別
不同的 DOM 節點可能具有不同的屬性。例如,對應於標籤 <a>
的元素節點具有連結相關屬性,而對應於 <input>
的元素節點具有輸入相關屬性,依此類推。文字節點與元素節點不同。但它們之間也有共同的屬性和方法,因為所有類別的 DOM 節點都形成單一層級。
每個 DOM 節點都屬於對應的內建類別。
層級的根源是 EventTarget,它由 Node 繼承,而其他 DOM 節點則從它繼承。
以下是圖片,說明如下
類別為
-
EventTarget – 是所有內容的根部「抽象」類別。
該類別的物件從未被建立。它用作基礎,讓所有 DOM 節點支援所謂的「事件」,我們稍後會研究它們。
-
Node – 也是一個「抽象」類別,用作 DOM 節點的基礎。
它提供核心樹狀功能:
parentNode
、nextSibling
、childNodes
等(它們是 getter)。Node
類別的物件從未被建立。但有其他類別從它繼承(因此繼承了Node
功能)。 -
Document,基於歷史原因,通常由
HTMLDocument
繼承(儘管最新的規範並未規定)– 是文件整體。document
全域物件恰好屬於此類別。它用作 DOM 的進入點。 -
CharacterData – 一個「抽象」類別,由下列項目繼承
-
Element – 是 DOM 元素的基礎類別。
它提供元素層級導覽,例如
nextElementSibling
、children
,以及搜尋方法,例如getElementsByTagName
、querySelector
。瀏覽器不僅支援 HTML,還支援 XML 和 SVG。因此,
Element
類別用作更具體類別的基礎:SVGElement
、XMLElement
(我們在此不需要它們)和HTMLElement
。 -
最後,HTMLElement 是所有 HTML 元素的基本類別。我們大部分時間都會使用它。
它由具體的 HTML 元素繼承
- HTMLInputElement –
<input>
元素的類別, - HTMLBodyElement –
<body>
元素的類別, - HTMLAnchorElement –
<a>
元素的類別, - …等等。
- HTMLInputElement –
還有許多其他標籤有自己的類別,可能具有特定屬性和方法,而某些元素,例如 <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.prototype
、Element.prototype
等。
console.dir(elem)
與 console.log(elem)
大多數瀏覽器在其開發人員工具中支援兩個指令:console.log
和 console.dir
。它們會將其引數輸出到主控台。對於 JavaScript 物件,這些指令通常會執行相同動作。
但對於 DOM 元素,它們是不同的
console.log(elem)
顯示元素 DOM 樹。console.dir(elem)
將元素顯示為 DOM 物件,很適合用來探索其屬性。
在 document.body
上試試看。
在規格中,DOM 類別並非使用 JavaScript 描述,而是使用一種特殊的 介面描述語言 (IDL),通常很容易理解。
在 IDL 中,所有屬性都以其類型為前綴。例如,DOMString
、boolean
等。
以下是摘錄,並附有註解
// 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 節點,我們可以從 nodeName
或 tagName
屬性讀取其標籤名稱
例如
alert( document.body.nodeName ); // BODY
alert( document.body.tagName ); // BODY
tagName
和 nodeName
有什麼不同?
當然,差異反映在它們的名稱中,但確實有點微妙。
tagName
屬性僅存在於Element
節點中。nodeName
定義於任何Node
中- 對於元素,它的意思與
tagName
相同。 - 對於其他節點類型(文字、註解等),它有一個包含節點類型的字串。
- 對於元素,它的意思與
換句話說,tagName
僅受元素節點支援(因為它源自 Element
類別),而 nodeName
可以說明其他節點類型。
例如,讓我們比較 document
和註解節點的 tagName
和 nodeName
<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>
如果我們只處理元素,那麼我們可以使用 tagName
和 nodeName
,沒有差別。
瀏覽器有兩種處理文件模式: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+=
執行下列動作
- 移除舊內容。
- 改寫新的
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
屬性僅對元素節點有效。
其他節點類型,例如文字節點,有其對應的 nodeValue
和 data
屬性。這兩個屬性在實際使用上幾乎相同,只有細微的規格差異。因此我們將使用 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>
- 第一個
<div>
取得名稱「以 HTML 方式」:所有標籤都變成標籤,因此我們會看到粗體名稱。 - 第二個
<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>
(HTMLInputElement
、HTMLSelectElement
…) 的值。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
時,與 CSSdisplay:none
相同。
DOM 節點也有其他屬性,具體取決於它們的類別。例如,<input>
元素(HTMLInputElement
)支援 value
、type
,而 <a>
元素(HTMLAnchorElement
)支援 href
等。大多數標準 HTML 屬性都有對應的 DOM 屬性。
但是,HTML 屬性和 DOM 屬性並不總是相同的,正如我們在下一章中所看到的。
註解
<code>
標籤,若要插入多行,請將它們包在<pre>
標籤中,若要插入超過 10 行,請使用沙盒 (plnkr、jsbin、codepen…)