導航DOM DOM中描述XML結構的方式是作為一種可導航的樹。使用的所有術語與一個家族樹(parents,children,sibling,等等)是近似的。與典型的家族樹不同的是,XML文檔以單個包含指向其子節點的指針的根節點(稱為文檔元素(
document element))開始。每一個子結節又包括指回其父結節、兄弟結點和子節點的指針。
DOM使用特定的術語來代表XML樹中的各種對象。DOM樹中的每一個對象都是一個
節點(node)[/i]。每個節點可以擁有不同的[i]類型(type),如元素(element),文本(text),或文檔(document)。為了繼續,我們需要等來了解DOM文檔是什麼樣子的以及怎樣在其中導航(一旦它已經構建完成)。通過一段簡單的HTML片段,我們來考察這一DOM構建工作是怎樣進行的。
<p><strong>Hello</strong> how are you doing?</p>
這個片斷的每一部分被分解成一個帶有指向基直接親屬(父、子、兄弟)的指針的DOM節點。如果完全描繪出存在的關係,它將會是類似於圖5-1。片段的每一部分(圓角盒子代表元素,方盒子代表文本節節點)與它所有的引用一起顯示。
[attach]33187[/attach]
圖5-1. 節點間的關係
每個DOM節點都包含一個指針的集合,它使用這些指針引用其親屬。你將使用這些指針來學習怎樣導航DOM。圖5-2顯示了所有可用的指針。這些屬性對每一個DOM節點都可用,是指向其它DOM元素的指針(或者是null,如果不存在對應元素的話)。
[attach]33188[/attach]
圖5-2. 使用指針導航DOM
僅使用指針而導航到頁麵的任何元素元素和文本塊是可能的。理解在現實環境裏這一點怎樣工作的最好的方式是來看一個普通的HTML頁麵,如程序5-1所示:
程序5-1. 一個簡單的HTML網頁,兼一個簡單的XML文檔
<html>
<head>
<title>Introduction to the DOM</title>
</head>
<body>
<h1>Introduction to the DOM</h1>
<p >There are a number of reasons why the
DOM is awesome, here are some:</p>
<ul>
<li >It can be found everywhere.</li>
<li >It's easy to use.</li>
<li >It can help you to find what you want, really quickly.</li>
</ul>
</body>
</html>
在這個示例文檔中,根元素是<html>元素。在JavaScript訪問這一個根元素是很輕鬆的:
如同它的DOM節點一樣,根結點擁有用來導航的所有指針。使用這些指針你就能開始瀏覽整個文檔,導航到你想要的任何元素。例如,要得到<h1>元素,你可能使用以下語句:
//Don't work!
document.documentElement.firstChild.nextSibling.firstChild
我們恰好撞上了我們的第一個暗礁:DOM指針既能指向文本節點也能指向元素。於是,上麵的語句實際並不是指向<h1>元素;它反倒指向<title>元素。為什麼會出這種事呢?這要歸咎於XML的最棘手和最受爭議的一個方麵:空白(white space)。你可能會注意到的,在<html>和<head>元素之間,實際上有一個換行符,它被認為是空白,這意味著那裏實際上首先有一個文本節點,而不是<head>元素。我們從中可以學到三件事:
1. 當嚐試隻使用指針導航DOM的時候,編寫漂亮、整潔的HTML標記可能反會使事情變得非常令人困惑。
2. 僅僅使用DOM指針導航文檔可能是非常的冗長和不實際。
3. 通常,你並不需要直接訪問文本節點,而是訪問包繞它們的元素。
這把我們導向一個問題:有沒有一種更好的方式用來在文檔找到元素呢?有!通過使用工具箱裏的幾個有用的函數,你可以輕易改善現有的方法,把DOM導航變得簡單得多。
處理DOM中的空白 讓我們先回到那個示例HTML文檔。先前,你試圖定位那個單獨的<h1>元素卻因無關的文本節點而遇上了困難。這對於單個的元素可能還是好的,但是倘若你想要找到<h1>後麵的那元素呢?你仍然會遭遇那個臭名昭著的空白bug,不得不使用.nextSibling.nextSibling來跳過<h1>和<p>之間的換行符。有一種技巧可以作為這一空白bug的補救辦法,如程序5-2所示。這一特別的技巧去除了DOM文檔所有的空白文本節點,便它變得更加易於穿行。這麼做對你的HTML怎麼渲染並沒有明顯的影響,卻能使用你手工導航變得容易。應該注意的是,這個函數的結果並不是永久性的,每次HTML文檔加載以後都需要重新運行。
程序5-2. XML文檔中空白bug的補救辦法
function cleanWhitespace( element ) {
//如果沒有提供element,則處理整個HTML文檔
element = element || document;
//使用firstChild作為開始指針
var cur = element.firstChild;
//遍曆所有子節點
while ( cur != null ) {
//如果該節點是文本節點,且隻含有空白
if ( cur.nodeType == 3 && ! //S/.test(cur.nodeValue) ) {
//刪除些文本節點
element.removeChild( cur );
//否則,如果它是一個元素
} else if ( cur.nodeType == 1 ) {
//遞歸處理下一級節點
cleanWhitespace( cur );
}
cur = cur.nextSibling; //移動到下一個子節點
}
}
比方說你想要在上麵的示例文檔這個函數以找到<h1>後麵的那個元素。完成這一工作的代碼會是類似這樣的:
cleanWhitespace();
//獲得文檔元素
document.documentElement
.firstChild //找到<head>元素
.nextSibling //找到<body>元素
.firstChild //找到<h1>元素
.nextSibling //取得相鄰的段落
這是一種既有好處又有缺點的技巧。最大的好處是,當你試圖導航DOM文檔的時候你可以保持某種程度的邏輯清晰。但是,考慮到你必須遍曆所有的DOM元素和文本節點來尋找隻包含空白的節點,這一技巧非常之慢。如果你的文檔包含大量的內容,它會明顯地拖慢你站點的加載。而且,每次你往文檔中注入新的HTML,你都需要重新掃描DOM的那一部分,確保沒有附加的空白文本節點被加入。
上述函數裏一個重要的方麵就是節點類型的使用。一個節點的類型可以通過檢查其nodeType屬性為特定值來判定。有許多種可能的值,但最經常碰到的是以下三種
元素(nodeType=1): 匹配XML文檔中的所有元素。例如,<li>,<a>,<p>,和<body>元素的nodeType全都是1。
文本(nodeType=3): 匹配文檔中所有的文本段。當在一個DOM結構中使用previousSibling的nextSibling導航時,你經常會在元素之間或元素內部遇到文本片段。
文檔(nodeType=9): 匹配一個文檔的根元素。比如,在一個HTML文檔裏,它就是<html>元素。
另外,(在非IE瀏覽器中)你可以使用常數來代表不同的節點類型。比如,代替記住1,3或9,你可以簡單地使用document.ELEMENT_NODE,document.TEXT_NODE,或document.DOCUMENT_NODE。既然反複地清除DOM的空白文本節點大有累贅之嫌,我們自然應該尋求其它的方法來導航DOM。
簡單的DOM導航 使用純DOM導航的原理(擁有每個方向的導航指針)你可以開發出可能更適合你的導航HTML DOM文檔的函數。這一特殊的原則的依據是:多數web開發者隻針對DOM元素而很少對其間的文本節點導航。下麵提供幾個函數,可以用來代替標準的previousSibling,nextSIbling,firstChild,lastChild以及parentNode。程序5-3展示了一個返回元素的前一個元素的函數。類似於元素的previousSibling屬性,如果沒有前一個元素,該函數返回null。
程序5-3. 用來查找元素的前一個兄弟元素的函數
function prev( elem ) {
do {
elem = elem.previousSibling;
} while ( elem && elem.nodeType != 1 );
return elem;
}
程序5-4展示了一個返回元素的下一個兄弟元素的函數。與元素的nextSibling屬性類似,當沒有下一個元素時,函數返回null。
程序5-4. 用來查找元素的後一個兄弟元素的函數
function next( elem ) {
do {
elem = elem.nextSibling;
} while ( elem && elem.nodeType != 1 );
return elem;
}
程序5-5展示了一個返回元素的第一個子元素的函數,與元素的firstChild屬性類似。
程序5-5.
function first( elem ) {
elem = elem.firstChild;
return elem && elem.nodeType != 1 ?
next ( elem ) : elem;
}
程序5-5展示了一個返回元素的最後一個子元素的函數,與元素的lastChild屬性類似。
程序5-6.
Listing 5-6. A Function for Finding the Last Child Element of an Element
function last( elem ) {
elem = elem.lastChild;
return elem && elem.nodeType != 1 ?
prev ( elem ) : elem;
}
程序5-7展示了一個返回元素父元素的函數,與元素的parentNode屬性類似。你可以提供一個可選的參數number,以一次向上移動幾層——比如說,parent(elem,2)與parent(parent(elem))等價。
程序5-7. 用來查找元素父元素的函數
function parent( elem, num ) {
num = num || 1;
for ( var i = 0; i < num; i++ )
if ( elem != null ) elem = elem.parentNode;
return elem;
}
使用這些新的函數,你可以快速地瀏覽一個DOM文檔,而無需擔心元素之間的文本。例如,為了找到<h1>元素的下一個元素,像從前一樣,你現在可以像下麵這麼做:
//查找<h1>的下一個元素
next( first( document.body ) )
注意到這行代碼的兩個特點。第一,有一個新的引用:document.body。所有現代瀏覽器都在HTML DOM文檔的body參數裏提供一個對<body>元素的引用。你可以利用這一點使你的代碼更加簡短和更加可理解。第二,函數的書寫方式是非常地違背直覺的。通常,當你想到導航時你可能會說:從<body>元素開始,得到第一個元素,再得到第二個元素。但是在它實際的書寫方式裏,好像是倒著來的。為了替代這一方式,我將會討論一些使得你定製的導航代碼更加清晰的辦法。
綁定到每一個HTML元素 在Firefox和Opera裏,有一種可用的非常強大的對象原型,稱為HTMLElement,它允許你將函數和數據附加到每個單獨的HTML DOM元素上。前麵一節所介紹的函數是很呆板的,可以進行某種清理。一種完美的方式是把你的函數直接綁定到HTMLElement原型上,以此來把它們直接綁定到每一個單獨的HTML元素。為了進行這一工作,對前麵所建立的函數需要作三個更改:
1. 在函數裏最上麵添加一行將使elem指向this,而不再是從參數列表取得。
2. 刪除那個你不再需要的元素參數。
3. 將函數綁定到HTMLElement原型,這樣你才能在第一個DOM中的HTML元素上使用它。
舉例來說,新的next函數將會是如程序5-8所示的樣子。
程序5-8. 向所有HTML DOM元素動態地綁定一個新的導航函數
HTMLElement.prototype.next = function() {
var elem = this;
do {
elem = elem.nextSibling;
} while ( elem && elem.nodeType != 1 );
return elem;
};
現在你可以像這樣使用next函數(或者是經過造的前述的第一個函數):
//一個簡單的例子:得到第一個的<p>元素
document.body.first().next()
這使得你的代碼更加清晰而易於理解,因為你能以自然思考的順序書寫代碼。如果你對這種書寫風格有興趣,我極力推薦你去看看JQuery庫,它極好地利用了這一技術。
注意:因為HTMLElement隻存在於三種現代瀏覽器中(Firefox,Safari,和Opera),你需要采取特殊的預防措施使它能夠在IE中工作。Jason Karl(https://browserland.org)編寫了一個特別便利的庫,在兩種不支持的瀏覽器中提供了對HTMLElement(及其它相關功能)的訪問。關於此庫的更多信息可以在這裏找到:https://www.browserland.org/scripts/htmlelement/。 標準的DOM方法 所有的現代DOM實現都包含幾種使工作更加有條理的方法。將它們和一些自定義函數結合使用,DOM導航將會變成一種流暢得多的體驗。首先,我們來看JavaScript DOM中包含的兩個功能強大的函數:
getElementById("everywhere"):此方法隻能應用於document對象,它在所有元素中查找ID等於everywhere的元素。這一強大的函數是立即訪問一個元素的最快的方式。
getElementsByTagName("li"):此方法可以在任意元素上使用,它在所有後代元素中查找標簽名為li的,並將它們作為一個(幾乎與數組相同的)節點列表返回。
警告:對HTML文檔來說,getElementById會如你想象的那樣工作:它檢查所有的元素直到找到id屬性與給定的值相同的那一個。然而,如果你載入一個遠程的XML文檔並使用getElementById(或使用JavaScript以外的另一種語言裏的DOM實現),它默認並不根據id屬性查找。它是由設計決定的;一個XML文檔必須明確地(一般用XML定義或XML模式)指定id屬性是什麼。 警告:getElementsByName返回一個節點列表。該結構外觀的行為都跟通常的JavaScript數組十分相似,但是有一個重要的例外:它不具有通常的.push(),.pop(),.shift()等等這些JavaScript數組所具有的方法。使用getElementsByName時牢記這一點,會省去你許多的疑惑。 這兩個方法在所有的現代瀏覽器中都是可用的,且對於定位特定元素極有幫助。回到前麵我們試圖找到<h1>元素的例子,現在我們可以像下麵這麼做:
document.getElementsByTagName("h1")[0]
這段代碼會有保障地工作並總是返回文檔中的第一個<h1>元素。回到前麵的示例文檔,假設你想到得到所有的<li>元素並給它們加上邊框:
var li = document.getElementsByTagName("li");
for ( var j = 0; j < li.length; j++ ) {
li[j].style.border = "1px solid #000";
}
再一次地,我們回頭看查找第一個<h1>元素後麵的元素的問題,完成這一工作的代碼到期可以減短得更多:
//找到第一個<h1>元素的後一個元素
next(tag("h1")[0]);
這些函數提供了快速得到你想操作的DOM元素的能力。在學習使用這一能力來修改DOM之前,你需要先快速地看看你的腳本第一次執行以後DOM加載的問題。
[
本帖最後由 mozart0 於 2007-4-15 09:51 編輯 ]