DOM

DOM是针对HTML和XML文档的一个API。DOM描绘了一个层次化的节点树,允许开发人员添加、移除和修改页面的某一部分。

IE中的所有DOM对象都是以COM对象的形式实现的。这意味着IE中的DOM对象与原生的JavaScript对象的行为或活动特点并不一致。

节点层次


<html>
    <head>Sample Page</head>
    <body>
        <p>Hello World!</p>
    </body>
</html>

文档节点是每个文档的根节点。这个例子中,文档节点只有一个子节点,即html元素,我们称之为文档元素。文档元素是文档的最外层元素,文档中的其他所有元素都包含在文档元素中。每个文档只能有一个文档元素。在HTML页面中,文档元素始终都是<html>元素。在XML中,没有预定义的元素,因此任何元素都可能成为文档元素。

每一段标记都可以通过树中的一个节点来表示。

Node 类型

DOM1级定义了一个Node接口,该接口将由DOM中所有节点类型实现。这个Node接口在JavaScript中是作为Node类型实现的;除了IE之外,在其他所有浏览器中都可以访问到这个类型。JavaScript中的所有节点类型都继承自Node类型,因此所有节点类型都共享着相同的基本属性和方法。

每个节点都有一个nodeType属性,用于表面节点的类型。节点类型由在Node类型中定义的下列12个数值常量来表示,任何节点类型必居其一:

  • Node.ELEMENT_NODE(1);
  • Node.ATTRIBUTE_NODE(2);
  • Node.TEXT_NODE(3);
  • Node.CDATE_SECTION_NODE(4);
  • Node.ENTITY_REFERENCE_NODE(5);
  • Node.ENTITY_NODE(6);
  • Node.PROCESSING_INSTRUCTION_NODE(7);
  • Node.COMMENT_NODE(8);
  • Node.DOCUMENT_NODE(9);
  • Node.DOCUMENT_TYPE_NODE(10);
  • Node.DOCUMENT_FRAGMENT_NODE(11);
  • Node.NOTATION_NODE(12);
if (someNode.nodeType == Node.ELEMENT_NODE) { //在IE中无效
    alert("Node is an element.");
}
//由于IE没有公开Node类型的构造函数,因此上面的的代码在IE中会导致错误
if (someNode.nodeType == 1) { //适用于所有的浏览器
    alert("Node is an element.");
}

并不是所有节点类型都收到Web浏览器的支持。

nodeName 和 nodeValue 属性

对于元素节点,nodeName中保存的始终是元素的标签名,而nodeValue始终为null。

节点关系

每个节点都有一个childNodes属性,其中保存着一个NodeList对象。NodeList是一种类数组对象,用于保存一组有序的节点,可以通过位置来访问这些节点。NodeList对象的独特之处在于,它实际上是基于DOM结构动态执行查询的结果,因此DOM结构的变化能够自动反映在NodeList对象中。

var firstChild = someNode.childNodes[0];
var secondChild = someNode.childNodes.item(1);
var count = someNode.childNodes.length;

另外,要注意length属性表示的是访问NodeList的那一刻,其中包含的节点数量。可以用Array.prototype.slice()方法将NodeList对象转化为数组。

//在IE8之前版本无效
var arrayOfNodes = Array.prototype.slice.call(someNode.childNodes, 0);
//由于IE8之前的版本将NodeList实现为一个COM对象。要想在IE中将NodeList转化为数组,必须手动枚举所有成员。
function convertToArray (nodes) {
    var array = null;
    try {
        array = Array.prototype.slice.call(nodes, 0); //针对非IE浏览器
    } catch (ex) {
        array = new Array();
        for (var i = 0, len = nodes.length; i < len; i++) {
            array.push(nodes[i]);
        }
    }
return array;
}

每个节点都有一个parentNode属性,该属性指向文档树中的父节点。包含在childNodes列表中的所有节点都具有相同的父节点。此外,包含在childNodes列表中的每个节点相互之间都是同胞节点。通过使用列表中每个节点的previousSiblingnextSibling属性,可以访问同一列表中的其他节点。列表中的第一个节点的previousSibling属性值为null,而列表中最后一个节点的nextSibling属性的值同样也为null。

if (someNode.nextSibling === null) {
    alert("Last node in the parent`s childNodes list.");
} else if (someNode.previousSibling === null) {
    alert("First node in the parent`s childNodes list.");
}

当然,如果只有一个节点,那么该节点的previousSiblingnextSibling属性都为null。

父节点的firstChildlastChild属性分别指向其childNodes列表中的第一个和最后一个节点。在只有一个子节点的情况下,firstChildlastChild指向同一个节点。如果没有子节点,那么firstChildlastChild的值均为null。

hasChildNodes()这个方法在节点包含一个或多个子节点的情况下返回true。

所有节点都有的最后一个属性是ownerDocument,该属性指向表示整个文档的文档节点。

操作节点

因为关系指针是只读的,所有DOM提供了一些操作节点的方法。
appendChild()用于向childNodes列表的末尾添加一个节点。更新完成后,appendChild()返回新增的节点。

var  returnedNode = someNode.appendChild(newNode);
alert(returnedNode == newNode); //true
alert(returnedNode.lastChild == newNode); //true

如果传入到appendChild()中的节点已经是文档的一部分了,那结果就是将该节点从原来的位置转移到新位置。
因此,如果在调用appendChild()时传入了父节点的第一个子节点,那么该节点就会成为父节点的最后一个子节点。

//someNode有多个子节点
var returnedNode = someNode.appendChild(someNode.firstChild);
alert(returnedNode == someNode.firstChild); //false
alert(returnedNode == someNode.lastChild); //true

insertBefore()方法可以把节点放在childNodes列表中某个特定的位置上。这个方法接收两个参数:要插入的节点和作为参考的节点。插入节点后,被插入的节点会变成参考节点的前一个同胞节点(previousSibling),同时被方法返回。如果参考节点是null,则insertBefore()appendChild()执行相同的操作。

//插入后成为最后一个子节点
returnedNode = someNode.insertBefore(newNode, null);
alert(newNode == someNode.lastChild); //true

//插入后成为第一个子节点
var returnedNode = someNode.insertBefore(newNode, someNode.firstChild);
alert(returnedNode == newNode); //true
alert(newNode == someNode.firstChild); //true

//插入到最后一个子节点前面
returnedNode = someNode.insertBefore(newNode, someNode.lastChild);
alert(newNode == someNode.childNodes[someNode, childNodes.length - 2]); //true

replaceChild()方法接收两个参数:要插入的节点和要替换的节点。要替换的节点将由这个方法返回并从文档树中被移除,同时由要插入的节点占据其位置。

//替换第一个子节点
var returnedNode = someNode.replaceChild(newNode, someNode.firstChild);

//替换最后一个子节点
returnedNode = someNode.replaceChild(newNode, someNode.lastChild);

在使用replaceChild()插入一个节点时,该节点的所有关系指针都会从被它替换的节点复制过来。尽管从技术上讲,被替换的节点仍然还在文档中,但它在文档中已经没有了自己的位置。

removeChild()方法接收一个参数,既要移除的节点。被移除的节点将成为方法的返回值。

//移除第一个子节点
var formerFirstChild = someNode.removeChild(someNode.firstChild);

//移除最后一个子节点
var formerLastChild = someNode.removeChild(someNode.lastChild);

通过removeChild()移除的节点仍然为文档所有,只不过在文档中已经没有了自己的位置。

如果在不支持子节点的节点上调用了这些方法,将会导致错误发生。

其他方法

有两个方法是所有类型的节点都有的:

  • cloneNode(),用于创建调用这个方法的节点的一个完全相同的副本。接收一个布尔值参数,表示是否执行深复制。在参数为true时,执行深复制,也就是复制节点及其整个子节点树;在参数为false,执行浅复制,即只复制节点本身。复制后返回的节点副本属于文档所有,但并没有为它制定父节点。
  • normalize(),这个方法唯一的作用就是处理文档树中的文本节点。由于解析器的实现或DOM操作等原因,可能会出现文本节点不包含文本,或者连接出现两个文本节点的情况。当某个节点上调用这个方法时,就会在该节点的后代节点中查找上述两种情况。如果找到了空文本节点,则删除它;如果找到相邻的文本节点,则将它们合并为一个文本节点。

cloneNode()方法不会复制添加到DOM节点中的JavaScript属性,例如事件处理程序等。这个方法只复制特性、(在明确指定的情况下也复制)子节点,其他一切都不会复制。IE在此存在一个BUG,即它会复制事件处理程序,所以我们建议在复制之前最好先移除事件处理程序。

Document 类型

在浏览器中,document对象是HTMLDocument(继承自Document类型)的一个实例,表示整个HTML页面。而且,document对象是window对象的一个属性,因此可以将其作为全局对象来访问。

Document节点具有下列特性:

  • nodeType的值为9
  • nodeName的值为“#document”
  • nodeValue的值为null
  • parentNode的值为null
  • ownerDocument的值为null
  • 其子节点可能是一个DocumentType(最多一个)、Element(最多一个)、ProcessingInstruction或Comment。

Document类型可以表示HTML页面或者其他基于XML的文档。不过,最常见的应用还是作为HTMLDocument实例的document对象。

在Safari、Firefox、Chrome和Opera中,可以通过脚本访问Document类型的构造函数和原型。但在所有浏览器中都可以访问HTMLDocument类型的构造函数和原型,包括IE8及后续版本。

文档的子节点
document对象的documentElement属性始终指向HTML页面中的<html>元素。

<html>
    <body>
    </body>
</html>
var html = document.documentElement; //取得对<html>的引用
alert(html === document.childNodes[0]); //true
alert(html === document.firstChild); //true

document对象还有一个body属性,直接指向<body>元素。

var body = document.body; //取得对<body>的引用

所有浏览器都支持document.documentElement和document.body属性。

Document另一个可能的子节点是DocumentType。通常将<!DOCTYPE>标签看出一个与文档其他部分不同的实体,可以通过doctype属性(在浏览器中是document.doctype)来访问它的信息。

var doctype = document.doctype; //取得对<!DOCTYPE>的引用

浏览器对document.doctype的支持差别很大:

  • IE8及之前的版本:如果存在文档类型声明,会将其错误地解释为一个注释并把它当作Comment节点;而document.doctype的值始终为null。
  • IE9+及Firefox:如果存在文档类型什么,则将其作为文档的第一个子节点;document.doctype是一个DocumentType节点,也可以通过document.firstChild或document.childNodes[0]访问同一个节点。
  • Safari、Chrome和Opera:如果存在文档类型声明,则将其解析,但不作为文档的子节点。document.doctype是一个DocumentType节点,但该节点不会出现在document.childNodes中。

从技术上说,出现在<html>元素外部的注释应该算是文档的子节点。然而,不同的浏览器在是否解析这些注释以及能否正确处理它们等方面,也存在很大差异。

<!-- 第一条注释 -->
<html>
    <body>
    </body>
</html>
<!-- 第二条注释 -->

从逻辑上讲,我们会认为document.childNodes中应该包含3项,但是实际上存在差异。

  • IE8及之前版本、Safari3.1及更高版本、Opera和Chrome只为第一条注释创建节点,不为第二条注释创建节点。结果,第一条注释就会成为document.childNodes中的第一个子节点。
  • IE9及更高版本会将第一条注释创建为document.childNodes中的一个注释节点,也会将第二条注释创建为document.childNodes中的注释子节点。
  • Firefox以及Safari3.1之前的版本完全忽略这两条注释。

文档信息
document对象还有一些标准的Document对象所没有的属性。

title属性包含着<title>元素中的文本。

//取得文档标题
var originalTitle = document.title;

//设置文档标题
document.title = "New page title";

URL属性中包含页面完整的URL(即地址栏中显示的URL)。
domain属性中只包含页面的域名。
referrer属性中保存着链接到当前页面的那个页面的URL。在没有来源的情况下,referrer属性中可能会包含空字符串。

//取得完整的URL
var url = document.URL;

//取得域名
var domain = document.domain;

//取得来源页面的URL
var referrer = document.referrer;

在这3个属性中,只有domain是可以设置的,但也有安全方面的限制。

//假设页面来自p2p.wrox.com域

document.domain = "wrox.com"; //成功

document.domain = "nczo.net"; //出错

当页面中包含来自其他子域的框架或内嵌框架时,能够设置document.domain就非常方便了。由于跨域安全限制,来自不同子域的页面无法通过JavaScript通信。而通过将每个页面的document.domain设置为相同的值,这些页面就可以互相访问对方包含的JavaScript对象了。
浏览器对domain属性还有一个限制,即域名一开始是松散的,那么不能将它再设置为紧绷的。

//假设页面来自于p2p.wrox.com域

document.domain = "wrox.com"; //松散的,成功
document.domain = "p2p.wrox.com"; //紧绷的,出错

所有浏览器中都存在这一限制,但IE8是实现这一限制的最早IE版本。

查找元素
getElementById(),接受一个参数:要取得的元素的ID。如果找到则返回该元素,没找到则返回null。这里的ID必须与页面中元素的id特性(attribute)匹配,包括大小写。

<div id="myDiv">Some text</div>
var div = document.getElementById("myDiv"); //取得<div>元素的引用

var div = document.getElementById("mydiv"); //无效的ID (在IE7及更早的版本可以)

IE8及较低版本不区分ID的大小写。
如果页面中多个元素的ID相同,getElementById()只返回文档中第一个出现的元素。
IE7及较低版本还为此方法添加了一个怪癖:name特性与给定ID匹配的表单元素(<input><textarea><button><select>)也会被方法返回。

<input type="text" name="myElement" value="Text field">
<div id="myElement">A div</div>
<!-- document.getElementById("myElement")会返回<input>元素 -->

getElementByTagName(),接受一个参数,即要取得的元素的标签名,返回的是包含零或多个元素的NodeList。在HTML文档中,这个方法会返回一个HTMLCollection对象,作为一个“动态”集合,该对象与NodeList非常相似。

var images = document.getElementById("img");
alert(images.length); //输出图像的数量
alert(images[0].src); //输出第一个图像元素的src特性
alert(images.item(0).src); //输出第一个图像元素的src特性

HTMLCollection对象有一个方法namedItem(),通过元素的name特性取得集合中的项。

<img src="myimage.gif" name="myImage">
var myImage = images.namedItem("myImage");

HTMLCollection还支持按名称访问项:

var myImages = images["myImage"];

对于HTMLCollection,在后台对数值索引就会调用item(),而对于字符串索引就会调用namedItem()

//取得文档中的所有元素
var allElements = document.getElementsByTagName("*");
//由于在IE将注释(comment)实现为元素(element),因此在IE中调用getElementByTagName("*")将会返回所有的注释节点。

getElementsByName()方法返回带有给定name特殊的所有元素。返回的也是一个HTMLCollection。

特殊集合

除了属性和方法,document对象还有一些特殊的集合。这些集合都是HTMLCollection对象。

  • document.anchors 包含文档中所有带name特性的<a>元素。
  • document.applets 包含文档中所有的<applet>元素。
  • document.forms 包含文档中所有的<form>元素,等于document.getElementsByTagName("form")
  • document.images 包含了文档中所有的<img>元素
  • document.links 包含了文档中所有的带href特性的<a>元素

上面集合始终可以通过HTMLDocument对象访问到,而且和HTMLCollection对象类似,集合中的项也会随着当前文档内容的更新而更新。

DOM 一致性检测
document.implementation属性提供了浏览器实现DOM的哪些部分的相应信息和功能的对象。
DOM1级只为document.implementation规定了一个方法hasFeature()。该方法接受两个参数:要检测的DOM功能的名称及版本号。支持的话就返回true。

var hasXmlDom = document.implementation.hasFeature("XML","1.0");
DOM功能.png

有时候返回true也不意味着一定和规范一致。

文档写入

write()writeln()方法都接受一个字符串参数,即要写入到输出流中的文本。write()会原样写入,而writeln()会在字符串的末尾添加一个换行符(\n)。

<html>
<head>
    <title>document.write() Example</title>
</head>
<body>
    <p>The current date and time is :
    <script>
        document.write("<strong>" + (new Date()).toString() + "</strong>");
    </script>
    </p>
</body>
</html>

open()close()分别用于打开和关闭网页的输入流。

Element类型

Element类型用于表现XML或HTML元素,提供了对元素标签名、子节点及特性的访问。Element节点具有以下特征:

  • nodeType的值为1;
  • nodeName的值为元素的标签名;
  • nodeValue的值为null
  • parentNode可能是Document或Element;
  • 其子节点可能是Element、Text、Comment、ProcessingInstruction、CDATASection或EntityReference

要访问元素的标签名,可以使用nodeNametagName(主要为了清晰起见)属性。

<div id="myDiv"></div>
var div = document.getElementById("myDiv");
alert(div.tagName); //"DIV"
alert(div.tagName == div.nodeName); //true

在HTML中,标签名始终是全部大写;而在XML中,标签名则始终会与源代码一样。

HTML元素

推荐阅读更多精彩内容