DOM 操作

DOM 操作

深入探索 HTMLElement 型別

在 JavaScript 標準化後的 20 多年裡,它取得了長足的進步。雖然在 2020 年,JavaScript 已經被廣泛應用於伺服器、資料科學,甚至是物聯網裝置中,但重要的是要記住它最流行的使用場景:Web 瀏覽器。

網站是由 HTML 和/或 XML 文件構成的。這些文件是靜態的,不會自動改變。文件物件模型 (DOM) 是瀏覽器實現的一種程式設計介面,旨在讓靜態網站具備互動功能。DOM API 可用於更改文件的結構、樣式和內容。該 API 功能極其強大,無數前端框架(jQuery、React、Angular 等)都是圍繞它構建的,從而使得動態網站的開發變得更加簡單。

TypeScript 是 JavaScript 的強型別超集,它為 DOM API 提供了型別定義。這些定義在任何預設的 TypeScript 專案中都是現成的。在 lib.dom.d.ts 中超過 20,000 行的定義裡,有一個型別格外引人注目:HTMLElement。該型別是 TypeScript 進行 DOM 操作的基石。

你可以瀏覽 DOM 型別定義 的原始碼。

基礎示例

給定一個簡化的 index.html 檔案

html
<!DOCTYPE html>
<html lang="en">
<head><title>TypeScript Dom Manipulation</title></head>
<body>
<div id="app"></div>
<!-- Assume index.js is the compiled output of index.ts -->
<script src="index.js"></script>
</body>
</html>

讓我們探索一個向 #app 元素新增 <p>Hello, World!</p> 元素的 TypeScript 指令碼。

ts
// 1. Select the div element using the id property
const app = document.getElementById("app");
// 2. Create a new <p></p> element programmatically
const p = document.createElement("p");
// 3. Add the text content
p.textContent = "Hello, World!";
// 4. Append the p element to the div element
app?.appendChild(p);

編譯並執行 index.html 頁面後,得到的 HTML 將會是

html
<div id="app">
<p>Hello, World!</p>
</div>

Document 介面

TypeScript 程式碼的第一行使用了全域性變數 document。檢查該變數可知,它是由 lib.dom.d.ts 檔案中的 Document 介面定義的。程式碼片段中包含了對兩個方法 getElementByIdcreateElement 的呼叫。

Document.getElementById

該方法的定義如下

ts
getElementById(elementId: string): HTMLElement | null;

傳入一個元素 ID 字串,它將返回 HTMLElementnull。此方法引入了最重要的型別之一:HTMLElement。它作為所有其他元素介面的基本介面。例如,程式碼示例中的 p 變數型別為 HTMLParagraphElement。同時請注意,此方法可能會返回 null。這是因為在執行前,該方法無法確定是否一定能找到指定的元素。在程式碼片段的最後一行,使用了新的可選鏈運算子來呼叫 appendChild

Document.createElement

該方法的定義是(我省略了已棄用的定義)

ts
createElement<K extends keyof HTMLElementTagNameMap>(tagName: K, options?: ElementCreationOptions): HTMLElementTagNameMap[K];
createElement(tagName: string, options?: ElementCreationOptions): HTMLElement;

這是一個過載函式定義。第二個過載是最簡單的,其工作方式與 getElementById 方法非常相似。傳入任何 string,它都會返回一個標準的 HTMLElement。正是這個定義使開發者能夠建立獨特的 HTML 元素標籤。

例如,document.createElement('xyz') 返回一個 <xyz></xyz> 元素,這顯然不是 HTML 規範所指定的元素。

有興趣的話,你可以使用 document.getElementsByTagName 與自定義標籤元素進行互動。

對於 createElement 的第一個定義,它使用了一些高階的泛型模式。最好將其拆解開來理解,從泛型表示式 <K extends keyof HTMLElementTagNameMap> 開始。此表示式定義了一個受約束HTMLElementTagNameMap 介面鍵的泛型引數 K。該對映介面包含了每個指定的 HTML 標籤名稱及其對應的型別介面。例如,以下是前 5 個對映值:

ts
interface HTMLElementTagNameMap {
"a": HTMLAnchorElement;
"abbr": HTMLElement;
"address": HTMLElement;
"applet": HTMLAppletElement;
"area": HTMLAreaElement;
...
}

某些元素沒有獨特的屬性,因此它們只返回 HTMLElement;但其他型別具有獨特的屬性和方法,所以它們返回各自特定的介面(這些介面將繼承或實現 HTMLElement)。

現在,來看 createElement 定義的剩餘部分:(tagName: K, options?: ElementCreationOptions): HTMLElementTagNameMap[K]。第一個引數 tagName 被定義為泛型引數 K。TypeScript 直譯器足夠聰明,可以從該引數中推斷出泛型引數。這意味著開發者在使用該方法時不必指定泛型引數;傳入 tagName 引數的任何值都會被推斷為 K,並可在定義的其餘部分中使用。這正是發生的情況:返回值 HTMLElementTagNameMap[K] 接收 tagName 引數並使用它來返回相應的型別。這個定義就是程式碼片段中的 p 變數獲得 HTMLParagraphElement 型別的原因。如果程式碼是 document.createElement('a'),那麼它將是一個 HTMLAnchorElement 型別的元素。

Node 介面

document.getElementById 函式返回一個 HTMLElementHTMLElement 介面繼承自 Element 介面,而 Element 又繼承自 Node 介面。這種基於原型的繼承允許所有 HTMLElement 使用標準方法的一個子集。在程式碼片段中,我們使用 Node 介面上定義的屬性將新的 p 元素新增到網站中。

Node.appendChild

程式碼片段的最後一行是 app?.appendChild(p)。前面的 document.getElementById 部分詳細說明了這裡使用了可選鏈運算子,因為 app 在執行時可能為 null。appendChild 方法的定義為:

ts
appendChild<T extends Node>(newChild: T): T;

此方法的工作方式與 createElement 方法類似,因為泛型引數 T 是從 newChild 引數中推斷出來的。T約束為另一個基礎介面 Node

childrenchildNodes 的區別

前面提到過,HTMLElement 介面繼承自 Element,而 Element 繼承自 Node。在 DOM API 中存在一個子元素 (children) 的概念。例如在以下 HTML 中,p 標籤是 div 元素的子元素:

tsx
<div>
<p>Hello, World</p>
<p>TypeScript!</p>
</div>;
const div = document.getElementsByTagName("div")[0];
div.children;
// HTMLCollection(2) [p, p]
div.childNodes;
// NodeList(2) [p, p]

獲取 div 元素後,children 屬性將返回一個包含 HTMLParagraphElementHTMLCollection 列表。而 childNodes 屬性將返回一個類似的 NodeList 節點列表。每個 p 標籤的型別仍然是 HTMLParagraphElement,但 NodeList 可以包含 HTMLCollection 列表所無法包含的其他 HTML 節點

修改 HTML,移除其中一個 p 標籤,但保留文字。

tsx
<div>
<p>Hello, World</p>
TypeScript!
</div>;
const div = document.getElementsByTagName("div")[0];
div.children;
// HTMLCollection(1) [p]
div.childNodes;
// NodeList(2) [p, text]

觀察這兩個列表是如何變化的。children 現在只包含 <p>Hello, World</p> 元素,而 childNodes 則包含一個 text 節點,而不是兩個 p 節點。NodeList 中的 text 部分就是包含文字 TypeScript! 的字面量 Nodechildren 列表不包含此 Node,因為它不被視為 HTMLElement

querySelectorquerySelectorAll 方法

這兩個方法都是獲取符合更獨特約束條件的 DOM 元素列表的絕佳工具。它們在 lib.dom.d.ts 中的定義為:

ts
/**
* Returns the first element that is a descendant of node that matches selectors.
*/
querySelector<K extends keyof HTMLElementTagNameMap>(selectors: K): HTMLElementTagNameMap[K] | null;
querySelector<K extends keyof SVGElementTagNameMap>(selectors: K): SVGElementTagNameMap[K] | null;
querySelector<E extends Element = Element>(selectors: string): E | null;
/**
* Returns all element descendants of node that match selectors.
*/
querySelectorAll<K extends keyof HTMLElementTagNameMap>(selectors: K): NodeListOf<HTMLElementTagNameMap[K]>;
querySelectorAll<K extends keyof SVGElementTagNameMap>(selectors: K): NodeListOf<SVGElementTagNameMap[K]>;
querySelectorAll<E extends Element = Element>(selectors: string): NodeListOf<E>;

querySelectorAll 的定義與 getElementsByTagName 相似,除了它返回一種新型別:NodeListOf。此返回型別本質上是標準 JavaScript 列表元素的自定義實現。可以說,將 NodeListOf<E> 替換為 E[] 會帶來非常相似的使用者體驗。NodeListOf 僅實現了以下屬性和方法:lengthitem(index)forEach((value, key, parent) => void) 以及數字索引。此外,此方法返回的是元素列表,而不是節點列表,而這正是 .childNodes 方法返回 NodeList 的情況。雖然這看起來是一個差異,但請注意 Element 介面繼承自 Node

要檢視這些方法的實際應用,請將現有程式碼修改為:

tsx
<ul>
<li>First :)</li>
<li>Second!</li>
<li>Third times a charm.</li>
</ul>;
const first = document.querySelector("li"); // returns the first li element
const all = document.querySelectorAll("li"); // returns the list of all li elements

想了解更多?

lib.dom.d.ts 型別定義最棒的地方在於,它們反映了 Mozilla 開發者網路 (MDN) 文件站點中註釋的型別。例如,HTMLElement 介面在 MDN 的 HTMLElement 頁面 上有詳細記錄。這些頁面列出了所有可用的屬性、方法,有時甚至包含示例。這些頁面的另一個優點是它們提供了指向相應標準文件的連結。這是 W3C HTMLElement 推薦標準 的連結。

來源

TypeScript 文件是一個開源專案。歡迎提交 Pull Request 來幫助我們改進這些頁面 ❤

此頁面的貢獻者
EAEthan Arrowood (6)
OTOrta Therox (5)
SASafei Ashraf (1)
MMateusz (1)
IOIván Ovejero (1)
6+

最後更新:2026 年 3 月 27 日