导航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 编辑 ]