文件物件模型(Document Object Model, DOM)

目錄

Document Object Model(簡稱 DOM)用來描述 HTML 或 XML 文件的邏輯架構與提供應用程式的撰寫介面(Application Programming Interface, API),程式設計師利用此 API 可以建立文件內容、巡訪文件結構、以及添加、修改、和刪除文件的元件與內容。

DOM 的沿革

Netscape DOM

我們之前所示範的 JavaScript 程式都採用 Netscape 公司所定義的 DOM,來存取某些種類的 HTML 元件,如 <img>, <a>, <form>, 和表單內的元件。 Navigator, IE 和大部分具有 JavaScript 功能的流覽器都支援此 DOM,所以它變成一個業界標準,通常被稱為第零級(level 0)的 DOM。由於此模型廣為新舊流覽器所支援,所以除非不得以,我們應儘量使用它來撰寫網頁程式,使得程式能在最多種類的流覽器中正確執行。

IE DOM

Microsoft 公司在 IE 4 中推出自己的 DOM 版本,爾後的 IE 5 和 IE 6 也都沿用。IE DOM 讓 JavaScript 程式可以存取網頁中所有的元件與屬性,因此提供比 Netscape DOM 更完整的功能。不過,IE DOM 只有 IE 流覽器支援,並不適用於其它的流覽器。

W3C DOM

由於 DOM 規格的分岐,W3C 開始制定 DOM 的標準,以作為各家流覽器支援 DOM 的準則。第一級(level 1)的DOM 標準(簡稱為 DOM1)於 1998 年十月公佈。 IE 5 和 IE 6 與以 Mozila 程式碼為基準開發出的流覽器(如 Netscape 6, Netscape 7, 和 FireFox)都支援此模型的絕大部分功能。

第二級(level 2) )的DOM 標準(簡稱為 DOM2)於 2000 年十一月公佈。此模型修正 level 1 DOM 的缺失,也定義事件、樣式表、…、等處理準則。此外,改用模組化的方式把 DOM 的規格加以分門別類,讓軟體廠商能依據所需選擇製作其中部分項目即可。IE 自第六版後,只是做小幅安全性的修正,並任何功能上的加強,所以對 level 2 DOM 的支援遠不如不斷更新的 Mozila 類流覽器來得完整,後者更以完全支援 W3C DOM 為目標。

第三級(level 2) )的DOM 標準(簡稱為 DOM3)於 2000 年十一月公佈。 此模型修正 level 2 DOM 的缺失,並對 XML 文件提供更完善的支援。

DOM 概觀

文件樹(Document Tree)

假定網頁的原始檔內容下:

<html>
  <head>
    <title>Sample Document</title>
  </head>
  <body>
    <h1>An HTML Document</h1>
    <p>This is a <i>simple</i> document.</p>
  </body>
</html>

則 DOM 把它組織成如下圖所示的樹狀結構:

上圖中的綠色方塊稱為 Node(節點)型態的物件,底下我們簡單說明 Node 介面所具有的屬性與方法。

節點型態

文件樹中的節點可以區分為幾種不同的型態。我們可以讀取 Node 物件屬性 nodeType 的值來判斷它的型態。下表列出最常見的節點型態:

Interfac nodeType 常數名稱 nodeTpe 值
Element Node.ELEMENT_NODE 1
Text Node.TEXT_NODE 3
Document Node.DOCUMENT_NODE 9
Comment Node.COMMENT_NODE 8
DocumentFragment Node.DOCUMENT_FRAGMENT_NODE 11
Attr Node.ATTRIBUTE_NODE 2

 

用於巡訪文件樹的屬性

屬性名稱 意義
childNodes 節點的所有子節點(是一個陣列值)
firstChild 節點的第一個子節點
lastChild 節點的最後一個子節點
nextSibling 節點的下一個兄弟節點
previousSibling 節點的上一個兄弟節點
parentNode 節點的父節點

 

Node 物件提供的方法

方法名稱 用途
appendChild(Node newChild) newChild 附加成節點的最後一個子節點
removeChild(Node oldChild) 移除節點的 oldChild 子節點
replaceChild(Node newChild, Node oldChild) newChild 來置換節點的oldChild 子節點
insertBefore(Node newChild, Node refChild) newChild 加在節點的refChild 子節點之前
cloneNode(boolean deep) 若參數 deep 為 true,則傳回此節點與其所有子孫節點的複製內容,否則只傳回此節點的複製內容。
hasChildNodes() 若節點有子節點,則傳回 true,否則傳回 false。
normalize() 把此節點的子孫節點中,相鄰的文字型態節點合而為一。通常用於插入或刪除節點之後,使文件樹的結構得以簡化。

 

DOM HTML API 的架構

DOM 以物件導向(object oriented)的方式來建構其 API,然而,DOM 採用介面(interface)而非類別(class)的定義方式。下圖是 DOM HTML API 的精簡圖。Node 是此介面階層的根節點,它提供其下所有子介面前述的共同屬性與方法。每一個流覽器視窗(或框架)都有一個型態為 HTMLDocument 的物件作為文件樹的根節點(即以前所介紹的 document 物件),其中除了繼承 Document 介面的屬性與方法外,也包含 DOM level 0 的屬性與方法,以便和早期的網頁程式相容。元件內容中的文字組合成 Text 型態的物件。

 

每一個 HTML 元件都是 HTMLElement 型態,其中包含下列六個屬性:id, style, title, lang, dir 和 className,這些屬性分別對應 HTML 元件的屬性:id, style, title, lang, dir 和 class。只具有這六個屬性的 HTML 元件(見下表),其對應物件的型態即設定為 HTMLElement。

<abbr> <acronym> <address> <b> <bdo>
<big> <center> <cite> <code> <dd>
<dfn> <dt> <em> <i> <kbd>
<noframes> <noscript> <s> <samp> <small>
<spaon> <strike> <strong> <sub> <sup>
<tt> <u> <var>    

其它具有額外屬性的 HTML 元件,DOM 則定義與其對應的 HTMLElement 之子介面,在其中加入適當的屬性。譬如:<body> 元件屬於 HTMLBodyElement 介面,其中包含 bgColor, backGround, link 等等專屬的屬性。

HTML 元件的屬性名稱不區分大小寫,然而 JavaScript 的識別字名稱卻有大小寫之分,因此 DOM 用以下的規則來命名元件介面中對應 HTML 元件的屬性名稱:

存取元件物件的屬性

我們可以用以下的簡便方法來存取元件物件的屬性:

element.someattr = someValue;

var attrvalue = element.someattr;

其中,element 是某個元件物件,someattr 是它的一項屬性。我們也可以用下表所列的 Element 介面方法來存取屬性:

方法名稱 用途
getAttribute(String name) 傳回名為 name 的屬性值(字串型態)
setAttribute(String name, String value) 把名為 name 的屬性設值成 value
removeAttribute(String name) 移除名為 name 的屬性
hasAttribute(String name) 若元件包含名為 name 的屬性,則傳回 true,否則傳回 false。(DOM2)

 

巡訪文件樹中的節點

我們在這一節用一些範例來介紹巡訪文件樹的技巧。

範例一: 底下的 countTag(n) 函式可用來計算節點 n 及其下包含多少 Element 型態的節點:

function countTags(n) { 						// n is a Node 
  var numtags = 0; 							// Initialize the tag counter
  if (n.nodeType == 1 /*Node.ELEMENT_NODE*/) 	// Check if n is an Element
    numtags++; 								// Increment the counter if so
  var children = n.childNodes; 				// Now get all children of n
  for(var i=0; i < children.length; i++) { 	// Loop through the children
    numtags += countTags(children[i]); 		// Recurse on each one
  }
  return numtags; // Return total number of tags
}

示範網頁

範例二: 底下的 countCharacters(n) 函式可用來計算節點 n 及其下的文字內容包含多少字元:

function countCharacters(n) {				// n is a Node
  if (n.nodeType == 3 /*Node.TEXT_NODE*/)	// Check if n is a Text object
    return n.length;						// If so, return its length.
  // Otherwise, n may have children whose characters we need to count
  var numchars = 0;						// Used to hold total characters of the children
  // Instead of using the childNodes property, this loop examines the
  // children of n using the firstChild and nextSibling properties.
  for(var m = n.firstChild; m != null; m = m.nextSibling) {
    numchars += countCharacters(m);		// Add up total characters found
  }
  return numchars; // Return total characters
}

示範網頁

尋找特定的元件

HTMLDocument 介面提供以下的方法讓你取得特定的元件:

方法名稱 用途
getElementById(String elementID) 傳回 id 屬性值為 elementID 的元件
getElementsByTagName(String tagname ) 傳回一個 Element 型態的陣列,其中依序擺放標籤名為 tagname 的元件
getElementsByName(String elementname ) 傳回一個 Element 型態的陣列,其中依序擺放 name 屬性值為 elementname 的元件

 

範例三: 底下的程式行用警示視窗來顯示網頁所包含的 <table> 元件個數:

var tables = document.getElementsByTagName("table");
alert("This document contains " + tables.length + " tables");

 

範例五: 假定網頁中包含底下的元件:

<p id="specialParagraph">

則我們可以用以下的式子來取得此元件:

var myParagraph = document.getElementById("specialParagraph");

 

修改文件結構

我們在這一節用一些範例來介紹修改文件結構的技巧。

範例六: 底下的 reverse(n) 函式反轉節點 n 的子節點順序:

function reverse(n) {          // Reverse the order of the children of Node n
var kids = n.childNodes; // Get the list of children
var numkids = kids.length; // Figure out how many there are
for(var i = numkids-1; i >= 0; i--) { // Loop through them backwards
var c = n.removeChild(kids[i]); // Remove a child
n.appendChild(c); // Put it back at its new position
}
}

示範網頁

範例七: 底下是遞迴式的 reverse(n) 函式:

// Recursively reverse all nodes beneath Node n, and reverse Text nodes
function reverse(n) {
if (n.nodeType == 3 /*Node.TEXT_NODE*/) { // Reverse Text nodes
var text = n.data; // Get content of node
var reversed = "";
for(var i = text.length-1; i >= 0; i--) // Reverse it
reversed += text.charAt(i);
n.data = reversed; // Store reversed text
}
else { // For non-Text nodes recursively reverse the order of the children
var kids = n.childNodes;
var numkids = kids.length;
for(var i = numkids-1; i >= 0; i--) { // Loop through kids
reverse(kids[i]); // Recurse to reverse kid
n.appendChild(n.removeChild(kids[i])); // Move kid to new position
}
}
}

範例八: 底下的 uppercase(n) 函式把節點 n 及其下節點的文字內容全改成大寫字母:

function uppercase(n) {
if (n.nodeType == 3 /*Node.TEXT_NODE*/) {
// If the node is a Text node, create a new Text node that
// holds the uppercase version of the node's text, and use the
// replaceChild() method of the parent node to replace the
// original node with the new uppercase node.
var newNode = document.createTextNode(n.data.toUpperCase());
var parent = n.parentNode;
parent.replaceChild(newNode, n);
}
else {
// If the node was not a Text node, loop through its children,
// and recursively call this function on each child.
var kids = n.childNodes;
for(var i = 0; i < kids.length; i++) uppercase(kids[i]);
}
}

示範網頁

在這個例子中,我們使用 Document 介面提供的 createTextNode() 方法來新建一個 Text 型態的節點。 下表列出這一類的方法:

方法名稱 用途
createAttribute(String name ) 傳回一個名稱為 name 的 Attr 型態節點
createComment(String data ) 傳回一個內容為 data 的 Comment 型態節點
createElement(String tagname ) 傳回一個標籤為 tagname 的 Element 型態節點
createTextNode(String data ) 傳回一個內容為 data 的 Text 型態節點

 

範例九: 底下的 embolden(node) 函式把節點 node 放在 <b> 元件中:

// This function takes a node n, replaces it in the tree with an Element node
// that represents an html <b> tag, and then makes the original node the
// child of the new <b> element.
function embolden(node) {
var bold = document.createElement("b"); // Create a new <b> Element
var parent = node.parentNode; // Get the parent of node
parent.replaceChild(bold, node); // Replace node with the <b> tag
bold.appendChild(node); // Make node a child of the <b> tag
}

示範網頁

範例十: 底下的maketoc(replace) 函式搜尋網頁原始檔中的 h1, h2, ..., h3 標籤在網頁的前端建立內容目錄,並使得目錄中的項目連結至對應的標題位置。你可以在網頁原始檔中用以下的方式來呼叫 maktoc() 函式:

<body onload="maketoc(document.getElementById('placeholder'))">
<!-- This span element will be replaced by the generated TOC -->
<span id="placeholder">Table of Contents</span>
... rest of document goes here ...

/**
* Create a table of contents for this document, and insert the TOC into
* the document by replacing the node specified by the replace argument.
**/
function maketoc(replace) {
// Create a <div> element that is the root of the TOC tree
var toc = document.createElement("div");
// Set a background color and font for the TOC. We'll learn about
// the style property in the next chapter
toc.style.backgroundColor = "white";
toc.style.fontFamily = "sans-serif";

// Start the TOC with an anchor so we can link back to it.
var anchor = document.createElement("a"); // Create an <a> node
anchor.setAttribute("name", "TOC"); // Give it a name
toc.appendChild(anchor); // And insert it // Make the body of the anchor the title of the TOC
anchor.appendChild(document.createTextNode("Table Of Contents"));

// Create a <table> element that will hold the TOC and add it
var table = document.createElement("table");
toc.appendChild(table);

// Create a <tbody> element that holds the rows of the TOC
var tbody = document.createElement("tbody");
table.appendChild(tbody);
// Initialize an array that keeps track of section numbers
var sectionNumbers = [0,0,0,0,0,0]; // Recursively traverse the body of the document, looking for sections
// marked with <h1>, <h2>, ... tags, and use them to create the TOC
// by adding rows to the table.
addSections(document.body, tbody, sectionNumbers);

// Finally, insert the TOC into the document by replacing the node
// specified by the replace argument with the TOC sub-tree
replace.parentNode.replaceChild(toc, replace);

// This method recursively traverses the tree rooted at node n, looking
// for <h1> through <h6> tags and uses the content of these tags to build
// the table of contents by adding rows to the HTML table specified by the
// toc argument. It uses the sectionNumbers array to keep track of the
// current section number.
// This function is defined inside of maketoc() so that it is not
// visible from the outside. maketoc() is the only function
// exported by this JavaScript module.
function addSections(n, toc, sectionNumbers) {
// Loop through all the children of n
for(var m = n.firstChild; m != null; m = m.nextSibling) {
// Check whether m is a heading element. It would be nice if we
// could just use (m instanceof HTMLHeadingElement), but this is
// not required by the specification and it does not work in IE.
// Therefore we've got to check the tagname to see if it is H1-H6.
if ((m.nodeType == 1) && /* Node.ELEMENT_NODE */
(m.tagName.length == 2) && (m.tagName.charAt(0) == "H")) {
// Figure out what level heading it is
var level = parseInt(m.tagName.charAt(1));
if (!isNaN(level) && (level >= 1) && (level <= 6)) {
// Increment the section number for this heading level
sectionNumbers[level-1]++;
// And reset all lower heading level numbers to zero
for(var i = level; i < 6; i++) sectionNumbers[i] = 0;
// Now combine section numbers for all heading levels
// to produce a section number like 2.3.1
var sectionNumber = "";
for(var i = 0; i < level; i++) {
sectionNumber += sectionNumbers[i];
if (i < level-1) sectionNumber += ".";
}

// Create an anchor to mark the beginning of this section.
// This will be the target of a link we add to the TOC.
var anchor = document.createElement("a");
anchor.setAttribute("name", "SECT"+sectionNumber);

// Create a link back to the TOC and make it a
// child of the anchor
var backlink = document.createElement("a");
backlink.setAttribute("href", "#TOC");
backlink.appendChild(document.createTextNode("Contents"));
anchor.appendChild(backlink);
// Insert the anchor into the document right before the
// section header
n.insertBefore(anchor, m);

// Now create a link to this section. It will be added
// to the TOC below.
var link = document.createElement("a");
link.setAttribute("href", "#SECT" + sectionNumber);
// Get the heading text using a function defined below
var sectionTitle = getTextContent(m);
// Use the heading text as the content of the link.
link.appendChild(document.createTextNode(sectionTitle));

// Create a new row for the TOC
var row = document.createElement("tr");
// Create two columns for the row
var col1 = document.createElement("td");
var col2 = document.createElement("td");
// Make the first column right-aligned and put the section
// number in it
col1.setAttribute("align", "right");
col1.appendChild(document.createTextNode(sectionNumber));
// Put a link to the section in the second column
col2.appendChild(link);
// Add the columns to the row, and the row to the table
row.appendChild(col1);
row.appendChild(col2);
toc.appendChild(row);

// Modify the section header element itself to add
// the section number as part of the section title
m.insertBefore(document.createTextNode(sectionNumber+": "),
m.firstChild);
}
}
else { // Otherwise, this is not a heading element, so recurse
addSections(m, toc, sectionNumbers);
}
}
}

// This utility function traverses the node n, returning the content of
// all text nodes found, and discarding any HTML tags. This is also
// defined as a nested function so that it is private to this module.
function getTextContent(n) {
var s = '';
var children = n.childNodes;
for(var i = 0; i < children.length; i++) {
var child = children[i];
if (child.nodeType == 3 /*Node.TEXT_NODE*/) s += child.data;
else s += getTextContent(child);
}
return s;
}
}

 

本章參考資料

David Flanagan. JavaScript The Definitive Guide 4th Ed. O'Reilly. 2002.