DOM 操作

DOM 操作

探討 HTMLElement 類型

自標準化以來 20 多年以來,JavaScript 已走過漫漫長路。雖然在 2020 年,JavaScript 可用於伺服器、資料科學,甚至 IoT 裝置,但請務必記住其最受歡迎的用例:網頁瀏覽器。

網站由 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>

讓我們探討一個 TypeScript 程式碼,它會將 <p>Hello, World!</p> 元素新增到 #app 元素。

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 方法。傳遞任何 字串,它將會傳回標準的 HTMLElement。此定義讓開發人員能夠建立獨特的 HTML 元素標籤。

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

對於有興趣的人,你可以使用 document.getElementsByTagName 與自訂標籤元素互動

對於 createElement 的第一個定義,它使用了一些進階的泛型模式。最好將其分解成區塊來理解,從泛型表達式開始:<K extends keyof HTMLElementTagNameMap>。此表達式定義了一個泛型參數 K,它 受限 於介面 HTMLElementTagNameMap 的鍵。此對應介面包含每個指定的 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 介面。這個原型延伸允許所有 HTMLElements 使用標準方法的子集。在程式碼片段中,我們使用在 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 中,有一個概念稱為子項元素。例如在下列 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 屬性會傳回一個包含 HTMLParagraphElementsHTMLCollection 清單。childNodes 屬性會傳回一個類似的 NodeList 節點清單。每個 p 標籤仍會是 HTMLParagraphElements 類型,但 NodeList 可以包含 HTMLCollection 清單無法包含的其他HTML 節點

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

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 節點。NodeListtext 部分是包含文字 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 清單元素的客製化實作。可以說,用 E[] 取代 NodeListOf<E> 會產生非常類似的使用者體驗。NodeListOf 只實作下列屬性和方法:lengthitem(index)forEach((value, key, parent) => void),以及數字索引。此外,此方法會傳回 元素 清單,而非 節點,而這是 NodeList.childNodes 方法傳回的內容。雖然這看起來像是有出入,但請注意 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 Developer Network (MDN) 文件網站中註解的類型。例如,HTMLElement 介面是由 MDN 上的 HTMLElement 頁面 記錄的。這些頁面會列出所有可用的屬性、方法,有時甚至還有範例。這些頁面的另一個優點是,它們會提供連結到對應標準文件的連結。以下是 HTMLElement 的 W3C 建議 的連結。

來源

TypeScript 文件是一個開源專案。透過 傳送 Pull Request ❤,協助我們改善這些頁面

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

最後更新:2024 年 3 月 21 日