DOM 簡介
September 13, 2022在瀏覽器開始支援 JavaScript 時,存在一組物件提供有限的功能,在當時 Netscape Navigator 與 Internet Explorer 大戰的年代,兩個瀏覽器在這組物件上有些交集的部份,這些交集的部份被留存到現在,並在一些主流的瀏覽器中仍有支援。這組物件稱為瀏覽器物件模型(Browser Object Model)或非正式地稱為 Level 0 DOM,因為它在 DOM(Document Object Model)標準化前就已存在,而不是真有有文件規範 Level 0 DOM。
Level 0 DOM
Level 0 DOM 從全域物件 window
開始,具有以下的特性階層關係:
window|
|navigator
|location
|frames
|screen
|history
|document
|forms
|links
|anchors
|images
|all
|cookie
這些物件在一般 JavaScript 的文件都有介紹,使用上雖然不難,然而有些會有跨瀏覽器的問題(以下列的參考鏈結,大致有標出適用哪些瀏覽器),以下僅作每個物件的簡介。
window
物件代表瀏覽器視窗本身,為 Window
的實例,是 JavaScript 以瀏覽器為客戶端時的頂層物件,其中像是 alert
、confirm
、prompt
、setTimeout
、clearTimeout
、setInterval
、clearInterval
等函式,都是以 window
作為名稱空間物件的函式。
window
本身也擁有一些控制視窗的方法,例如 open
、moveTo
、scroll
、scrollTo
等。可以在〈Window Object〉 查詢這些函式或方法的使用方式。
window.navigator
代表了瀏覽器,為 Navigator
的實例,可以從這個物件上取得瀏覽器本身的資訊,通常是為了進行所謂瀏覽器偵測才會使用這個物件,不過這個物件取得的資訊並非完全可靠,因為有的瀏覽器提供有選項或者有工具可變造這些資訊。你可以在〈Navigator Object〉查詢這個物件上有哪些資訊可以取得。
window.location
代表了視窗目前所顯示頁面的 URL,是 Location
的實例,可以從這個物件上取得目前頁面的 URL 相關資訊,也有 reload
與 replace
方法,可以重新載入頁面或取代頁面。可在〈Location Object〉 查詢這個物件的相關內容。也有個 window.document.location
,與 window.location
是同一個物件。
window.frames
收集了視窗中擁有的 iframe
對應物件,以類陣列的 HTMLCollection
物件組織,索引位置是框架在視窗中出現的順序,如果框架有設定 id
或 name
屬性,也可以使用 []
搭配名稱來取得框架,每個取得的框架都是 Window
的實例,透過 JavaScript 可以取得 iframe
的內容,例如:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<iframe id="same-origin" src="same-origin.html"></iframe>
<span id="console"></span>
<script type="module">
let doc = window.frames['same-origin'].document;
console.log(doc.body.innerHTML);
</script>
</body>
</html>
不過這個方式僅限同源網頁,另一方面,現在不少伺服端會實現內容安全策略(Content-Security-Policy),限制網頁被內嵌於 iframe
的條件,一旦被限制內嵌,瀏覽器就不會呈現畫面,更別說是透過 JavaScript 來取得內容了。
瀏覽器寬高等相關的資訊,可以在 window
物件上取得:
- 視窗在螢幕中的位置:
screenX
、screenY
- 視窗寬、高:
outerWidth
、outerHeight
- 視埠區域寬高(不包括選單、工具列、捲軸):
innerWidth
、innerHeight
- 水平、垂直捲軸位置:
pageXOffset
、pageYOffset
window.screen
物件包括了目前視窗所關聯的螢幕資訊,像是寬、高、顏色深度等,為 Screen
的實例,可以在〈Screen Object〉查詢各個特性,常用的有:
- 螢幕的寬高,可以使用
screen
的width
、height
取得。 - 螢幕可用區域的寬高,不含工具列的範圍,可在
screen
的availWidth
、availHeight
取得。
window.history
則包括了瀏覽器瀏覽歷史,為 History
實例,基於安全與隱私,你無法取得瀏覽歷史,但可以有 back
、forward
、go
等方法,指定前進、後退至哪個歷史頁面,像是回到上一面、下一頁的功能,想使用 JavaScript 控制,就可以用這個物件,可查詢〈History Object〉了解細節。
window.document
為 Document
的實例,代表整份 HTML 文件,這個物件上提供一組群集(Collection)物件,它們都是 HTMLCollection
的實例,這些群集物件都有個特徵,本身可以用 []
搭配索引數字來存取,如果相對應的標籤有 id
或 name
屬性,也可以使用 []
搭配名稱來存取,或是搭配點算子與名稱來存取(先前介紹的 window.frames
也有這種特徵)。
(HTMLCollection
規範的是使用 item
方法搭配索引來取得元素,或使用 namedItem
方法搭配 id
或 name
屬性來取得元素,不過在 JavaScript 實作中,可以且通常使用 []
搭配索引或名稱來取得元素。)
舉個來說,如果有個網頁如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<form name="login">
User: <input type="text" name="user" value="guest"><br>
Password: <input type="password" name="passwd" value="guest">
<button type="submit">Submit<button/>
</form>
</body>
</html>
想要取得文件中 name
為 login
的表單,有幾種方式:
document.forms[0] // 文件中第一個表單,索引為 0
document.forms['login']
document.forms.login
forms
、links
、anchors
、images
等,都可以用這種方式來取得。
在透過 document.forms
取得某個表單元素之後,如果想取得其下的子元素,可以使用 elements
來取得,一樣可以使用索引或 name
搭配 []
來取得,也可以透過 .
運算子來取得。例如,想取得 name
為 user
的輸入文字框,可以如下:
document.forms['login'].elements[0] // 表單中第一個元素
document.forms['login'].elements['user']
document.forms['login'].elements.user
表單比較特別,如果你的表單有 name
屬性,而當中的元素也有 name
屬性,則可以有個很方便的存取方式。例如若要取得 login
表單的 user
欄位,則可以如下:
document.login.user
由於輸入欄位有個 value
屬性,所以要取得欄位值就是:
document.login.user.value
只要你取得標籤元素,標籤上的某個屬性,基本上可以在物件上使用相對應的特性來取得,除了一些保留字之外,例如 class
屬性要用 className
特性名稱來取得,因為 class
是保留字,而 <label>
的 for
屬性要用 htmlFor
特性來取得。
超鏈結與錨點都是使用 <a>
來定義,差別在於超鏈結使用 href
屬性,而錨點使用 name
屬性, document.links
與 document.anchors
則分別表示超鏈結與錨點元素。
documents.images
則表示了文件中的所有 <img>
元素。
document.all
代表文件中所有元素,是早期存取文件中所有元素的方式。例如,若文件中某個元素具有 name
屬性值為 'element1'
,則可以透過這種方式來取得:
document.all['element1']
在 DOM 標準化之後,這種寫法的作用已經被 document.getElementsByName
等方法取代了。
document
的 cookie
則允許設定與讀取 cookie
,設定方式是字串,形式為 name=value
,每個 name/value 間以分號區隔,取得的也是字串,你必須自行剖析 name/value 對。可以參考〈Cookie〉稍微了解一下 Cookie 的原理。
基於安全性,被標示了 HttpOnly
屬性的 Cookie,無法使用 JavaScript 來讀取。
W3C DOM
W3C 聯合各瀏覽器廠商制訂了標準物件模型,試圖讓各瀏覽器廠商遵合此一模型進行實作,以解決各瀏覽器間物件模型不一致的問題,在新的物件模型中,也對文件操作的功能加以擴充。
簡單來說,在 DOM 的標準下,一份文件中所有的標籤定義,包括文字,都是一個物件,這些物件以文件定義的結構,形成了一個樹狀結構。例如:
<html>
<head>
<title>首頁</title>
</head>
<body>
<h1>Hello!World!</h1>
<a href="zh-tw/">學習筆記</a>
</body>
</html>
這份 HTML 文件,會形成以下樹狀的物件結構:
document (Document)
|-html (HTMLHtmlElement)
|-head (HTMLHeadElement)
| |-title (HTMLTitleElement)
| |-首頁 (Text)
|
|body (HTMLBodyElement)
|-h1 (HTMLHeadingElement
| |-Hello!World! (Text)
|
|-a (HTMLAnchorElement)
|-學習筆記 (Text)
右邊的括號,表示每個物件的型態。注意,document
代表整個文件,而不代表 html
標籤節點,你可以使用 document.childNodes[0]
取得 html
標籤 DOM 元素,childNodes
表示取得子節點,取回的會是 NodeList
物件,是個類陣列物件,可使用索引值來指定取得某個子節點。
方便的 document.documentElement
也可用來取得 html
標籤 DOM 元素。如果想取得 body
標籤 DOM 元素,也可以透過 document.body
來取得。注意,文字也會形成樹狀結構中的元素。
儘管你在上面看到的元素形態,有許多都帶有 HTML
字眼,但 DOM 並非專屬於 HTML 的物件模型,DOM API 分為兩部份,一個是核心 DOM API,一個是 HTML DOM API。
核心 API 是一個獨立的規範,可以任何語言實現,可操作的對象是基於 XML 的任何文件,你可以在〈XML DOM Tutorial〉找到 DOM 核心 API 的相關資料。
HTML API 是 核心 API 的延伸,專門操作 HTML,各種物件對應的形態,通常會有個 HTML 字眼在前頭,你可以在〈JavaScript and HTML DOM Reference〉找到 HTML DOM API 的相關資料。
核心 API 文件中所有內容都視為節點,包括文件本身,再依類型區分出不同的形態:
Node
|Document
|Element
|Text
|Attr
...
Document
代表整份文件,Element
是所有標籤(也是節點),Text
代表文字元素(也是節點)。
Level 0 DOM 在 window
物件上有 navigator
、location
、frames
、screen
、history
等與瀏覽器相關的物件,與文件相關的物件,實際上只有 document
,功能也有限,這個部份納入了 DOM 標準,成為了 DOM 的子集,由於這些舊式 API 都是專屬於 HTML,所以你在〈Document〉的文件上看不到相關操作,而必須在〈The HTML DOM Document Object〉這個專屬於 HTML 的 DOM API 文件上才可以找到。
同樣地,Element
僅定義核心 DOM API 中元素的操作,而 HTMLHeadElement
等元素也有一些專屬 HTML 的 API,這在〈Element〉文件中找不到,而必須在〈The HTML DOM Document Object〉等相關類型中尋找。
每個節點都會有 nodeName
與 nodeType
特性,前者可以取得節點的名稱,後者可以取得節點型態常數,這個常數用來查找對應的型態名稱,常數與型態名稱的對照可在〈HTML DOM nodeType Property
Element Object〉文件中找到。
舉例來說,可以搭配 JavaScript,如下顯示出一個網頁的節點與型態:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>首頁</title>
</head>
<body>
<h1>Hello!World!</h1>
<a href="https://openhome.cc/zh-tw/">學習筆記</a>
<div id="console"></div>
<script type="text/javascript">
let typeNames = new Map([
[Node.ELEMENT_NODE, 'ELEMENT_NODE'],
[Node.ATTRIBUTE_NODE, 'ATTRIBUTE_NODE'],
[Node.TEXT_NODE, 'TEXT_NODE'],
[Node.CDATA_SECTION_NODE, 'CDATA_SECTION_NODE'],
[Node.ENTITY_REFERENCE_NODE, 'ENTITY_REFERENCE_NODE'],
[Node.ENTITY_NODE, 'ENTITY_NODE'],
[Node.PROCESSING_INSTRUCTION_NODE, 'PROCESSING_INSTRUCTION_NODE'],
[Node.COMMENT_NODE, 'COMMENT_NODE'],
[Node.DOCUMENT_NODE, 'DOCUMENT_NODE'],
[Node.DOCUMENT_TYPE_NODE, 'DOCUMENT_TYPE_NODE'],
[Node.DOCUMENT_FRAGMENT_NODE, 'DOCUMENT_FRAGMENT_NODE'],
[Node.NOTATION_NODE, 'NOTATION_NODE']
]);
function subNodesOf(parent, indent = ' ') {
let nodes = parent.childNodes;
let nodeDesc = Array.from(nodes).reduce((nodeDesc, node) => {
let nodeName = node.nodeName;
let typeName = typeNames.get(node.nodeType);
return nodeDesc + `${indent} ${nodeName} ${typeName} <br>` +
subNodesOf(node, ` ${indent}`);
}, '');
return nodeDesc;
}
let nodeName = document.nodeName;
let typeName = typeNames.get(document.nodeType);
document.getElementById('console').innerHTML =
`${nodeName} ${typeName} <br>` + subNodesOf(document);
</script>
</body>
</html>
要取得文件寬高資訊,可以使用的特性有:
- 要取得 HTML 文件寬高,可以在
document.documentElement
以scrollWidth
、scrollHeight
取得。 - 要取得
body
寬高,則可以使用document.body
的scrollWidth
、scrollHeight
取得。