-
DOM
(文档对象模型)是针对HTML和XML文档的一个API(应用程序编程接口)。DOM描绘了一个层次化的节点树,允许开发人员添加、移除和修改页面的某一部分。DOM
脱胎于Netscape及微软公司创始的DHTML(动态HTML),但现在它已经成为表现和操作页面标记的真正跨平台、语言中立方式。 - 1998年10月DOM 1 级规范成为W3C的推荐标准,为基本的文档结构及查询提供了接口。本章主要讨论与浏览器中的HTML页面相关的DOM1级的特性和应用,以及JavaScript对DOM1级的视线。
- IE中的所有DOM对象都是以
COM
对象的形式实现的。这意味着IE中的DOM
对象与原生JavaScript对象的行为或活动特点并不一致。本章将较多的谈及这些差异。
节点层次
<html>
<head>
<title>Sample Page</title>
</head>
<body>
<p>Hello World!</p>
</body>
</html>
- 可以将这个简单的HTML文档视为一个层次结构,如图10-1所示。
- 文档节点是每个文档的根节点。在这个例子中,文档节点只有一个子节点,既
<html>
元素,我们称之为文档元素。 - 文档元素是文档的最外层元素,文档中的其他所有元素都包含在文档元素中。每个文档只能有一个文档元素。在HTML页面中,文档元素始终都是
<html>
元素。在XML中,没有预定义的元素,因此任何元素都可能成为文档元素。
Node 类型
- DOM1级定义了一个
Node
接口,该接口将由 DOM 中所有节点类型实现。这个Node接口在JavaScript中是作为Node
类型实现的;除了IE之外,在其他所有浏览器中都可以访问到这个类型。 - JavaScript中的所有节点类型都继承自
Node
类型,因此所有节点类型都共享着相同的基本属性和方法。 -
每个节点都有一个
nodeType
属性,用于表明节点的类型。及诶单类型由在Node
类型中定义的下列12个数值常量来表示,任何节点类型必居其一(编号为节点类型常量存储的数值):Node.ELEMENT_NODE
Node.ATTRIBUTE_NODE
Node.TEXT_NODE
Node.CDATA_SECTION_NODE
Node.ENTITY_REFERENCE_NODE
Node.ENTITY_NODE
Node.PROCESSING_INSTRUCTION_NODE
Node.COMMENT_NODE
Node.DOCUMENT_NODE
Node.DOCUMENT_TYPE_NODE
Node.DOCUMENT_FRAGMENT_NODE
Node.NOTATION_NODE
// 通过比较上面的常量,很容易的确定节点类型
// 在IE中无效
if (someNode.nodeType == Node.ELEMENT_NODE) {
console.log("Node is an element");
}
// 由于IE没有公开 Node 类型的构造函数
// 最好还是将 nodeType 属性与数字比较
if (someNode.nodeType == 1) {
console.log("Node is an element");
}
- 并不是所有节点类型都受到Web浏览器的支持。开发人员最常用的就是元素和文本节点。
nodeName
和 nodeValue
属性
- 了解节点的具体信息,可以使用
nodeName
和nodeValue
两个属性。这两个属性的值完全取决于节点类型。在使用这两个值以前,最好用上述的代码检查节点的类型。
if (someNode.nodeType == 1) {
value = someNode.nodeName; // nodeName的值是元素的标签名
}
节点关系
- 每个节点都有一个
childNodes
属性,其中保存着一个NodeList
对象。注意,可以通过方括号语法来访问NodeList
的值,而且也有length
属性,但它并不是Array
的实例。 -
NodeList
对象的独特之处在于,它实际上是基于DOM结构动态执行查询的结果,因此DOM结构的变化能够自动反映在NodeList
对象中。我们常说NodeList
是有生命、有呼吸的对象,而不是我们第一次访它的瞬间拍摄下来的一张快照。
// 方括号和 item() 语法结果是相同的
var firstChild = someNode.childNodes[0];
var secondChild = someNode.childNodes.item(1);
var count = comeNode.childNodes.length;
// 虽然不是Array的实例,但我们可以将它转换成数组
// 在IE8及之前的版本中无效
var arrayOfNodes = Array.prototype.slice.call(someNode.childNodes, 0);
// 由于IE8及更早版本将 NodeList 实现为一个 COM 对象
// 必须手动枚举所有成员,才能转换成数组
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
中的所有节点都具有相同的父节点,而相互之间是同胞节点。 - 通过每个节点的
previousSibling
和nextSibling
属性可以访问同一列表中的其他节点。列表第一个节点previousSibling
为null
,列表最后一个nextSibling
为null
,当然如果列表只有一个节点,那么两个都是null
。 - 父节点的
firstChild
和lastChild
属性分别指向第一个和最后一个。如果列表没有节点,那么两个属性都是null
。
-
hasChildNodes()
也是一个非常有用的方法,当查询节点存在子节点时返回true
,不存在返回false
。这是比查询childNodes.length
更简单的方法。 - 所有节点都有的最后一个属性是
ownerDocument
,该属性指向表示整个文档的文档节点。这种关系表示的是任何节点都属于它所在的文档,任何节点都不能同时存在两个或更多个文档中。通过这个属性,我们可以不必在节点层次中通过层层回溯达到顶端,而是可以直接访问文档节点。
操作节点
- 因为关系指针都是只读的,所以DOM提供了一些操作节点的方法 。
- 最常用的方法是
appendChild()
,用于向childNodes
列表的末尾添加一个节点,执行后,方法返回新增的节点。
var returnedNode = someNode.appendChild(newNode);
console.log(returnedNode == newNode); // true
console.log(someNode.lastChild ==newNode); // true
- 如果需要把节点放在
childNodes
列表中某个特定的位置上,而不是放在末尾,可以使用insertBefore()
方法。这个方法接收两个参数:要插入的节点和作为参照的节点。插入节点后,被插入的节点会变成参照节点的前一个同胞节点(previousSibling),同时被方法返回。如果参照节点是null
,则insertBefore()
和appendChild()
执行相同操作。
// 插入后成为最后一个子节点
var returnedNode = someNode.insertBefore(newNode, null);
// 插入后成为第一个子节点
var returnedNode = someNode.insertBefore(newNode, someNode.firstChild);
// 插入后在最后一个子节点前面
var returnedNode = someNode.insertBefore(newNode, someNode.lastChild);
-
replaceChild()
替换节点。同样接收两个参数,插入的节点和参照节点。插入新的节点并将参照节点从文档树中移除,新的节点会从被替换的节点复制所有关系指针。尽管从技术上讲,被替换的节点仍然在文档中,但它在文档中的位置已经不存在了。 -
removeChild()
移除节点。被移除的节点仍然在文档中,但它在文档中的位置已经不存在了。 - 以上四个方法必须先取得操作节点的父节点(代码示例中是someNode)。在不存在子节点的节点上调用以上方法,会导致错误。
其他方法
- 还有两个方法是所有节点都有的。
-
cloneNode()
用于创建调用这个方法的节点的一个完全相同的副本。接收一个布尔值参数,表示是否执行深复制。- 传入true。执行深复制,复制节点及其整个子节点树
- 传入false。执行浅复制,即只复制节点本身。
- 复制返回的节点副本属于文档所有,但并没有为它制定父节点。因此这个节点副本就成为了一个“孤儿”,除非通过
appendChild()
insertBefore()
replaceChild()
将它添加到文档中。 - IE8及之前的版本不会为包含空白符的文字创建节点(TEXT)
-
clone()
方法不会复制添加到DOM节点中的JavaScript属性,例如时间处理程序。这个方法只复制特性、(在明确指定的情况下也复制)子节点,其他一切都不会复制。 - IE 会复制事件处理程序,所以我们建议在复制之前最好先移出事件处理程序。
<ul id="ul">
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
</ul>
var myList = document.getElementById("ul");
var deepList = myList.cloneNode(true);
// [text, li, text, li, text, li, text]
console.log(deepList.childNodes);
// 3 (IE < 9) 或 7 (其他浏览器)
// IE8及之前的版本不会为包含空白符的文字创建节点(TEXT)
console.log(deepList.childNodes.length);
var shallowList = myList.cloneNode(false);
console.log(shallowList.childNodes.length); // 0
-
normalize()
方法唯一的作用就是处理文档树中的文本节点。由于解析器的实现或DOM操作等原因,可能会出现文本节点不包含文本,或者连接出现两个节点的情况。当在某个节点上调用这个方法时,就会在该节点的后代节点中查找上述两种情况。- 如果找到了空文本节点,则删除它
- 如果找到相邻的文本节点,则将它们合并为一个文本节点
- 本章后面还将进一步讨论方法
var html = document.documentElement; // 取得对<html>的引用
console.log(html == document.childNodes[0]); // true
console.log(html == document.firstchild) // true
- 所有浏览器都支持
document.documentElement
和document.boyd
属性 - Document另一个可能的子节点是DocumentType。通常将
<!DOCTYPE>
标签看成一个与文档其他部分不同的实体,可以通过doctype属性(在浏览器中是document.doctype
)来访问信息。 -
浏览器对
document.doctype
的支持差别很大,所以这个属性的用途很有限:- IE8及之前版本,如果存在文档类型声明,会将其错误的解释为一个注释并把它当做Comment节点;而
document.doctype
的值始终为null
- IE9+,如果存在文档类型声明,则将其作为文档的第一个子节点;
document.doctype
是一个DocumentType节点,也可以通过document.firstChild
或document.childNodes[0]
访问同一个节点。 - Safari, Chrome, Opera :如果存在文档类型声明,则将其解析,但不作为文档的子节点。
document.doctype
是一个DocumentType节点,但该节点不会出现在document.childNodes
中。
- IE8及之前版本,如果存在文档类型声明,会将其错误的解释为一个注释并把它当做Comment节点;而
- 从技术上说,出现在
<html>
元素外部的注释应该是算是文档的子节点。然而,不同的浏览器在是否解析这些注释以及能否正确处理他们等方面,也存在很大差异。
<!-- 第一条注释 -->
<html>
<body>
</body>
</html>
<!-- 第二条注释 -->
-
看起来这个页面应该有3个子节点:注释、
<html>
元素、注释。从逻辑上讲,我们会认为document.childNodes
中应该包含与这3个节点对应的3项。但是实际上,浏览器存在以下差异:- IE8及之前版本、Safari3.1及更高版本、Opera和Chrome 只为第一条注释创建节点,不为第二条注释创建节点。结果第一条注释就会成为
document.childNodes
中的第一个子节点。 - IE9+,将会将两条都创建节点。
- Firefox 和 Safari3.1之前的版本会完全忽略这两条注释。
- IE8及之前版本、Safari3.1及更高版本、Opera和Chrome 只为第一条注释创建节点,不为第二条注释创建节点。结果第一条注释就会成为
- 多数情况下,我们都用不着在
document
对象上调用appendChild()
removeChild()
replaceChild()
方法,因为文档类型(如果存在的话)是只读的,而且它只能有一个元素子节点(该节点通常早就已经存在了)。
文档信息
- 作为HMLTDocument的一个实例,
document
对象还有一些标准的Document对象所没有的属性。 -
title
包含着<title>
元素中的文本。通过这个属性可以取得当前页面的标题,也可以修改当前页面的标题并反映在浏览器的标题栏中。修改title
属性的值会改变<title>
元素。
// 取得文档标题
var originalTitle = document.title;
// 设置文档标题
document.title = "New page title";
-
下面三个属性与网页的请求有关,所有这些信息都存在于请求的HTTP头部,只不过是通过这些属性让我们能够在JavaScript中访问它们而已:
-
URL
属性中包含页面完整的URL(地址栏中的URL) -
domain
属性中值包含页面的域名 -
referrer
属性中可能会包含空字符串 -
URL
与domain
属性是相互关联的。例如document.URL
等于"http://www.wrox.com/WileyCDA/",那么document.domain
就等于"www.wrox.com"。 - 3个属性中只有
domain
可以设置,但有安全方面的限制。如果URL中包含一个子域名,例如"p2p.wrox.com",那么就只能讲domain
设置为"wrox.com"(URL中包含"www",如"www.wrox.com"时,也是如此)。 - 当页面中包含来自其他子域的框架或内嵌框架时,能够设置
document.domain
就非常方便了。由于跨域安全限制,来自不同子域的页面无法通过JavaScript通信。而通过将每个页面的document.domain
设置为相同的值,这些页面就可以互相访问对方包含的JavaScript对象了。
-
// 取得完整的URL
var url = document.URL;
// 取得域名
var domain = document.domain;
// 取得来源 页面的URL
var referrer = document.referrer;
- 浏览器对
domain
属性还有一个限制,即如果域名一开始是“松散的”(loose),那么就不能将它再设置为“紧绷的”(tight)。
// 假设页面来自于 p2p.wrox.com域
document.domain = "wrox.com"; // 松散的(成功)
document.domain = "p2p.wrox.com"; // 紧绷的(出错)
查找元素
-
getElementById()
接收一个参数:要取得的元素的ID。找到相应的元素则返回该元素,否则返回null
。- IE8及较低版本不区分ID大小写
- 如果页面多个元素的ID相同,只会返回第一个匹配的元素。
- IE7及更早的版本添加了一个怪癖:name特性与给定ID匹配的表单元素也会被该方法返回。
<input type="text" name="myElement" value="Text field"> <div id="myElement">A div</div> <script> // IE7中调用会返回<input>元素 var el = document.getElementById("myElement"); </script>
-
getElementsByTagName()
接收一个参数:要取得的元素的标签名,而返回的是包含零或多个元素的NodeList
。可以使用方括号语法或item()
方法来访问对象中的项。 -
namedItem()
使用这个方法可以通过元素的name特性取得集合中的项。或方括号语法能达到同样的效果
<img src="myimage.gif" name="myImage">
<script>
var images = document.getElementsByTagName("img");
console.log(images.length);
console.log(images[0].src); // 方括号传入数值就调用 item()
console.log(images.item(0).scr);
var myImage = images.namedItem("myImage");
var myImage = images["myImage"]; // 方括号传入字符串就调用namedItem()
</script>
- 要取得文档中的所有元素,可以向
getElementsByTagName()
中传入"*"。在JavaScript及CSS中,星号通常表示全部。 - 虽然标准规定标签名需要区分大小写,但为了最大限度的与既有HTML页面兼容,传给
getElementsByTagName()
的标签名是不需要区分大小写的。但对于XML页面而言(包括XHTML),getElementsByTagName()
方法就会区分大小写。 -
getElementByName()
是只有HTMLDocument类型才有的方法,返回带有给定name属性的所有元素。最常使用的情况是取得单选按钮;为了确保发送给浏览器的值正确无误,所有单选按钮必须具有相同的name特性
<fieldset>
<legend>Which color do you prefer?</legend>
<ul>
<li>
<input type="radio" value="red" name="color" id="colorRed">
<label for="colorRed">Red</label>
</li>
<li>
<input type="radio" value="green" name="color" id="colorGreen">
<label for="colorGreen">Green</label>
</li>
<li>
<input type="radio" value="blue" name="color" id="colorBlue">
<label for="colorBlue">Blue</label>
</li>
</ul>
</fieldset>
- 上述例子使用
getElementsByName()
方法可以返回三个input元素。但是对于这里的单选按钮来说namedItem()
方法只会取得第一项(因为每一项的name特性都相同)。
特殊集合
-
document.anchors
包含文档中所有带name特性的<a>
元素 -
document.applets
包含文档中所有的<form>
元素,与document.getElementsByTagName("form")
得到的结果相同 -
document.images
包含文档中所有的<img>
元素,与document.getElementsByTagName("img")
得到的结果相同 -
document.links
包含文档中所有带href
特性的<a>
元素
DOM一致性检测
- 由于DOM分为多个级别,也包含多个部分,因此检测浏览器实现了DOM的哪些部分就十分必要。
document.implementation
属性就是为此提供的,与浏览器对DOM的实现直接对应。 - DOM1级别只为
document.implementation
规定了一个方法,即hasFeature()
。接收两个参数:要检测的DOM功能的名称及版本号。如果支持返回true
var hasXmlDom = docuemnt.implementation.hasFeature("XML", "1.0");
- 下表列出了可以检测的不同值得版本号
功能 | 版本号 | 说明 |
---|---|---|
Core | 1.0、2.0、3.0 | 基本的DOM,用于描述表现文档的节点树 |
XML | 1.0、2.0、3.0 | Core的XML拓展,添加了对CDATA、处理指令及实体的支持 |
HTML | 1.0、2.0 | XML的HTML拓展,添加了对HTML特有元素及实体的支持 |
Views | 2.0 | 基于某些样式完成文档的格式化 |
StyleSheets | 2.0 | 将样式表关联到文档 |
CSS | 2.0 | 对层叠样式表1级的支持 |
CSS2 | 2.0 | 对层叠样式表2级的支持 |
Events | 2.0, 3.0 | 常规的DOM事件 |
UIEvents | 2.0, 3.0 | 用户界面事件 |
MouseEvents | 2.0, 3.0 | 由鼠标引发的事件(click、mouseover等) |
MutationEvents | 2.0, 3.0 | DOM树变化时引发的事件 |
HTMLEvents | 2.0 | HTML4.01事件 |
Range | 2.0 | 用于操作DOM树种某个范围的对象和方法 |
Traversal | 2.0 | 遍历DOM树的方法 |
LS | 3.0 | 文件与DOM树之间的同步加载和保存 |
LS-Asnyc | 3.0 | 文件与DOM树之间的异步加载和保存 |
Validation | 3.0 | 在确保有效的前提下修改DOM树的方法 |
-
hasFeature()
方法确实方便,但也有缺点。因为实现者可以自行决定是否与DOM规范的不同部分保持一致。事实上,想让hasFearture()
针对所有值都有返回true很容易,但返回true有时候也不意味着实现与规范一致。 - 为此我们建议,在使用
hasFreatrue()
之外,还同时使用能力检测。
文档写入
-
write()
和writeln()
方法都接收一个字符串参数,即要写入到输出流中的文本。wirte()
会原样写入,而writeln()
则会在字符串的末尾添加一个换行符(n)。在页面加载的过程中,可以使用这两个方法动态的加入内容。 - 在包含JavaScript文件时,必须注意不能像下面的例子那样直接包含字符串"</script>",因为这会导致该字符串被解释为脚本块的结束,后面的代码将无法执行。使用转义"</script>"可以避免这个问题。
-
open()
和close()
分别用于打开和关闭网页的输出流。如果是在页面加载期间使用write()
或writeln()
方法,则不需要用到这两个方法。 - 严格型XHTML文档不支持文档吸入。对于那些按照
application/xml+xhtml
内容类型提供的页面,这两个方法也同样无效。
Element类型
-
Element
类型用于表现XML或XHTML元素,提供了对元素标签名、子节点及特性的访问。 -
Element
类型具有以下特征:-
nodeType
的值为1 -
nodeName
的值为元素的标签名 -
nodeValue
的值为null
-
parentNode
的值可能为Dcoment或Element - 其子节点可能是
Element
、Text
、Comment
、ProcessingInstruction
、CDATASection
、EntityReference
-
- 访问元素的标签名,可以使用
nodeName
属性,也可以是使用tagName
属性,这两个属性会返回相同的值。
var div = document.getElementById("myDiv");
console.log(div.tagName); // "DIV"
console.log(div.nodeName); // "DIV"
console.log(div.tagName == div.nodeName); // true
if (element.tagName == "div") {
// 不能这样比较,很容易出错
}
if (element.tagName.toLowerCase() == "div") {
// 推荐这样做(适用于任何文档)
}
HTML元素
-
所有HTML元素都由
HTMLElement
类型表示。HTMLElement
类型直接继承自Elment并添加了一些属性。每个HTML元素中都存在的下列标准特性:-
id
元素在文档中的唯一标识符 -
title
有关元素的附加说明信息,一般通过工具提示条显示出来 -
lang
元素内容的语言代码,很少使用 -
dir
语言的方向值为"ltr"(left-to-right 从左至右)或 "rtl" -
className
与元素的class特性对应,即为元素指定的CSS类。没有将这个属性命名为class是因为class是ECMAScript的保留字。
-
- 并不是对所有属性的修改都会在页面中直观的表现出来。对id或lang的修改对用户而言是透明不可见的。而对title的修改则只会在鼠标移动到这个元素之上时才会显示出来。对dir的修改会在属性重写的那一刻,立即影响页面中文本的左右对齐方式。修改className时,如果新类关联了与此前不同的CSS样式,就立即应用新的样式。
- 下面表格列出了所有HTML元素以及与之关联的类型(以斜体印刷的元素表示不推荐使用了)。注意表中的这些类型在Opera、Safari、Chrome、Firefox中都可以通过JavaScript访问,但在IE8之前的版本中,不能通过JavaScript访问。
取得特性
- 操作特性的DOM方法主要有三个,分别是
getAttribute()
、setAttribute()
、removeAttribute()
。
var div = document.getElemntByid("myDiv");
console.log(div.getAttribute("id")); // "myDiv"
console.log(div.getAttribute("class")); // "bd"
console.log(div.getAttribute("title")); // "Body Text"
console.log(div.getAttribute("lang")); // "en"
console.log(div.getAttribute("dir")); // "ltr"
- 注意,传递给
getAttribute()
的特性名与实际的特性名相同。因此想要得到class特性值,应该传入"class" 而不是"className",后者只在通过对象属性访问特性时才用。 - 如果给定的特性不存在,
getAttribute()
返回null
。 - 也可以取得自定义特性,即标准HTML语言中没有的特性的值。需要注意,特性的名称不区分大小写,即"ID" 和 "id" 代表的都是同一个特性。另外也要注意,根据HTML5规范,自定义特性应该加上data-前缀以便验证。
- 任何元素的所有特性,也都可以通过DOM元素本身的属性来访问。当然
HTMLElement
也会有5个属性与相应的特性一一对应。不过只有公认的(非自定义)特性才会以属性的形式添加到DOM对象中。例如可以通过div.id
访问div元素的id属性。不过自定义特性在Safari、Opera、Chrome、Firefox中是不存在的,但IE却会为自定义特性也创建属性。 - CSS通过
getAttribute()
访问时,返回的style特性值中包含的是CSS文本,而通过属性来访问它则会返回一个对象。由于style属性是用于以编程方式访问元素样式的(本章后面讨论),因此并没有直接映射到style特性。 - 时间处理程序(例如onclick)通过
getAttribute()
访问,返回的是相应的代码字符串。而在访问onclick属性时,则返回的是一个JavaScript函数(如果未在元素中指定相应特性,则返回null
)。这是因为onclick及其他事件程序属性本身就应该被赋予函数值。 - 由于存在上述差别,在通过JavaScript以编程方式操作DOM时,开发人员不经常使用
getAttribute()
方法,而只是使用对象的属性。只有在取得自定义特性值得情况下,才会使用getAttribute()
方法。 - 在IE7及以前版本中,通过
getAttribute()
访问style特性或onclick,返回的值与属性相同,都返回对象值或函数值。虽然IE8已经修复了这个bug,但不同IE版本间的不一致性,也是导致开发人员不适用getAttribute()
访问HTML特性的一个原因。
设置特性
- 与
getAttribute()
对应的方法时setAttribute()
这个方法接收两个参数:要设置的特性名和值。如果特性已经存在,setAttribute()
会以指定的值替换现有的值;如果特性不存在,则创建该属性并设置相应的值。 -
setAttribute()
方法既可以操作HTML特性也可以操作自定义特性。通过这个方法设置的特性名会统一转换为小写形式,即"ID"最终变成"id"。
div.setAttribute("id", "someOtherId");
div.id = "someOtherId";
// 添加自定义属性,该属性不会自动成为元素的特性
div.mycolor = "red";
div.getAttribute("mycolor"); // null ie除外
-
removeAttribute()
用于彻底删除元素的特性,调用这个方法不仅会清楚特性的值,而且也会从元素中完全删除特性。这个方法并不常用,IE6及以前版本不支持。
div.removeAttribute("class");
attributes属性
- Element 类型是使用 attributes 属性的唯一一个DOM节点类型 。
- attributes属性中包含一个
NamedNodeMap
,与NodeList
类似,也是一个动态集合。元素的每一个 特性都由一个Attr
节点表示,每个节点都保存在NamedNodeMap
对象中。 -
NamedNodeMap
对象拥有以下方法-
getNamedItem(name)
:返回nodeName
属性等于name
的节点 -
removeNamedItem(name)
:从列表中移除nodeName
属性 等于name的节点 -
setNameItem(node)
:向列表中添加节点,以节点的nodeName
属性为索引 -
item(pos)
:返回位于数字pos位置处的节点
-
-
attributes
属性中包含一系列节点,每个节点的nodeName
就是特性的名称,而节点的nodeValue
就是特性的值。
// 取得元素的id
var id = element.attributes.getNamedItem("id").nodeValue;
// 设置元素的id
element.attributes["id"].nodeValue = "someOtherId";
// 删除元素id,并返回被删除特性的Attr节点
var oldAttr = element.attributes.removeNamedItem("id");
// 传入一个新的特性节点
element.attributes.setNameItem(newAttr);
- 由于attributes的方法不够方便,因此开啊人员更多的会使用
getAttribute()
、removeAttribute()
、setAttribute()
方法。如果想要遍历元素特性,可以用attributes - 针对attributes对象中的特性,不同浏览器返回的顺序不同。
- IE7及更早版本返回HTML元素中所有可能的特性,包括没有指定的特性。返回100多个特性是常见的
// 迭代元素的每一个特性,然后构造成 name="value"字符串
function outputAttributes(element) {
var pairs = new Array(),
attrName,
attrValue,
i,
len;
for (i=0, len=elment.attributes.length; i < len; i++) {
attrName = element.attributes[i].nodeName;
attrValue = element.attributes[i].nodeValue;
// 针对 IE7- 做兼容
// 根据specified属性,只返回指定的特性
if (element.attributes[i].specified) {
paris.push(attrName + "=\"" + attrValue + "\"");
}
}
return pairs.join(" ");
}
创建元素
-
document.createElement()
方法可以创建新元素。只接收一个参数,即要创建元素的标签名,在HTML文档中不区分大小写,而在XML(包括XHTML)文档中,则是区分大小写。 -
document.createElement()
创建元素的同时,也为新元素设置了ownerDcoument
属性。此时还可以操作元素的特性,为它添加更多子节点。 - 由于新元素尚未被添加到文档树中,因此设置这些特性不会影响浏览器的显示。要把新元素添加到文档树,可以使用
appendChild()
insertBefore()
replaceChild()
方法。
// 创建
var div = document.createElement("div");
// 操作元素特性,添加子节点
div.id = "myNewDiv";
div.className = "box";
document.body.appendChild(div);
-
在IE中可以传入完整的元素标签,也可以包含属性(仅IE支持)。这样有助于避开在IE7及更早版本中动态创建元素的某些问题:
- 不能设置动态创建的
<iframe>
元素的name
特性 - 不能通过表单的
reset()
方法重设动态创建的<input>
元素(第13章讨论reset()方法) - 动态创建的
type
特性值为“reset”的<button>
元素重设不了表单 - 动态创建的一批
name
相同的单选按钮彼此毫无关系。
- 不能设置动态创建的
if (client.browser.id && client.browser.ie <= 7) {
var div = document.createElement("<div id=\"myNewDiv\" class=\"box\"></div>");
}
元素的子节点
- 元素可以有任意书目的子节点和后台节点,因为元素可以是其他元素的子节点。元素的
childNodes
属性中包含了它所有子节点,这些子节点可能是元素、文本节点、注释或处理指令。不用浏览器在看待这些节点方面存在显著的不同。
<ul id="myList">
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
</ul>
- IE解析,
<ul>
元素会有3个子节点,分别是3个<li>
元素。但如果是其他浏览器,<ul>
元素都会有7个元素,包括3个<li>
元素和4个文本节点(表示<li>
元素之间的空白符)。 - 如果将元素间的空白符删除,那么所有浏览器都会返回相同数目的子节点
<ul id="myList"><li>item 1</li><li>item 2</li><li>item 3</li></ul>
- 如果需要通过
childNodes
属性遍历子节点,那么一定不要忘记浏览器间的这一差别。这意味着在执行某项操作以前,通常都要先检查nodeType
属性
for (var i=0, len = element.childNodes.length; i < len; i++) {
if (element.childNodes[i].nodeTpe == 1) {
...
}
}
- 如果想通过某个特性的标签名取得子节点或后代节点,可以通过元素调用
getElementsByTagName()
方法,结果只会返回当前元素的后代。
var ul = document.getElementById("myList");
var items = ul.getElementsByTagName("li");
Text类型
- 文本节点由Text类型表示,包含的是可以照字面量解释的纯文本内容。纯文本中可以包含转义后的HTML字符,但不能包含HTML代码。
-
Text节点具有以下特征:
-
nodeType
的值为3 -
nodeName
的值为'#text' -
nodeValue
的值为节点所包含的文本 -
parentNode
是一个Element - 不支持(没有)子节点
-
- 可以通过
nodeValue
属性或data
属性访问Text节点中包含的文本,这两个属性的值相同。对nodeValue
的修改也会通过data
反映出来,反之亦然。 -
使用下列方法可以操作节点中的文本
-
appendData(text)
:将text添加到节点的末尾 -
deleteData(offset, count)
:从offset指定的位置插入text -
insertData(offset, text)
:在offset指定的位置插入text -
replaceData(offset, count, text)
:用text替换从offset指定的位置开始到 offset+count为止处的文本 -
splitText(offset)
:从offset指定的位置将当前文本节点分成两个文本节点。 -
substringData(offset, count)
:提取从offset指定的位置开始到 offset+count为止处的字符串 -
length
属性:保存着节点中字符的书目。而且nodeValue.length
和data.length
中也保存着同样的数值
-
- 在默认情况下,每个可以包含内容的元素最多只能有一个文本节点,而且必须确实有内容存在
<!-- 没有内容,也就没有文本节点 -->
<div></div>
<!-- 有空格,因为有一个文本节点 -->
<div> </div>
<!-- 有内容,因为有一个文本节点 -->
<div>Hello World!</div>
// 可以像这样取得文本子节点
var textNode= div.firstChild; // 或者 div.childNodes[0]
// 取得文本节点的引用后,就可以修改它了
div.firstChild.nodeValue = "Some other message";
- 如果这个文本节点当前存在于文档树中,那么修改文本节点的结果就会立即得到反映。
- 修改文本节点时,字符串会经过HTML(或XML,取决于文档类型)编码。换言之,小于号、大于号或引号都会像下面的例子一样被转义
div.firstChild.nodeValue = "Some <strong>other</strong> message";
// 输出结果:"Some <strong>other</strong> message"
- 这是在向DOM文档中插入文本之前,先对其进行HTML编码的一种有效方式
创建文本节点
-
document.createTextNode()
创建新的文本节点。与设置已有文本节点的值一样,作为参数的文本也将按照HTML或XML的格式进行编码。
var textNode = document.createTextNode("<strong>Hello</strong> World!");
- 在创建新文本节点的同时,也会为其设置
ownerDocument
属性。不过除非把新节点添加到文档树中已经存在的节点中,否则我们不会在浏览器窗口中看到新节点。
var element = document.createElement("div");
elment.className = "message";
var textNode = document.createTextNode("Hello world!");
element.appendChild(textNode);
document.body.appendChild(element);
- 一般情况下,每个元素只有一个文本子节点。不过在某些情况下也可能包含多个文字子节点。相邻的同胞文本节点,之间会连起来,中间不会有空格。
var element = document.createElement("div");
elment.className = "message";
var textNode = document.createTextNode("Hello world!");
element.appendChild(textNode);
var anotherTextNode = document.createTextNode("Yippee!");
element.appendChild(anotherTextNode);
document.body.appendChild(element);
规范化文本节点
- DOM文档中存在相邻的同胞文本节点很容易导致混乱,因为分不清文本节点之间的界限。于是催生了一个能够将相邻文本节点合并的方法。
-
normalize()
方法是由Node
类型定义的(因而在所有节点类型中都存在)。如果在一个包含多个文本节点的父元素上调用normalize()
方法,则会将所有文本节点合并成一个文本节点。
var element = document.createElement("div");
elment.className = "message";
var textNode = document.createTextNode("Hello world!");
element.appendChild(textNode);
var anotherTextNode = document.createTextNode("Yippee!");
element.appendChild(anotherTextNode);
document.body.appendChild(element);
console.log(element.childNodes.length); // 2
element.normalize();
console.log(element.childNodes.length); // 1
console.log(element.firstChild.nodeValue); // "Hello World!Yippee!"
- 浏览器在解析文档时永远不会创建相邻的文本节点,这种情况只会作为DOM操作的结果出现。
-
normalize()
有时候会导致IE6崩溃,IE7以上修复了此问题。
分割文本节点
-
splitText()
方法会将一个文本节点分割成两个。
var element = document.createElement("div");
elment.className = "message";
var textNode = document.createTextNode("Hello world!");
element.appendChild(textNode);
document.body.appendChild(element);
var newNode = element.firstChild.splitText(5);
console.log(element.firstChild.nodeValue); // "Hello"
console.log(newNode.nodeValue); // " World!"
console.log(element.childNodes.length); // 2
Comment类型
-
注释在DOM中是通过Comment类型来表示的。
Comment
节点具有以下特征:-
nodeType
的值为8 -
nodeName
的值为 "#comment" -
nodeValue
的值是注释的内容 -
parentNode
可能是Dcoment或Element - 不支持(没有)子节点
-
- Comment类型与Text类型继承自相同的基类,因此它拥有除
splitText()
之外的所有字符串操作方法。
<div id="myDiv"><!--A comment--></div>
var div = document.getElementById("myDiv");
var comment = div.firstChild;
console.log(comment.data); // "A comment"
- 使用
document.createComment()
并为其传递注释文本也可以创建注释节点
var comment = document.createComment("A comment ");
- 开发人员很少会创建和访问注释节点,此外浏览器也不会识别位于
</html>
标签后的注释。如果要访问注释节点,一定要保证它们是位于<html>
和</html>
之间。
CDATASection类型
- CDATASection类型只针对基于XML的文档,表示的是CDATA区域。与Comment类似、CDATASection类型继承自Text类型,因此拥有除
splitText()
之外的所有字符串操作方法。 -
CDATASection节点具有下列特征:
-
nodeType
的值为4 -
nodeName
的值为"#cdata-section" -
nodeValue
的值是CDATA区域中的内容 -
parentNode
可能是Document
或Element
- 不支持(没有)子节点
-
- CDATA区域只会出现在XML文档中,因此多数浏览器都会把CDATA区域错误的解析为Comment或Element。
<div id="myDiv"><![CDATA[This is some content.]]></div>
- 这个例子中div元素应该包含一个CDATASection节点。但四大主流浏览器都不能正确解析。即使对于有效的XHTML页面,浏览器也没有正确的支持嵌入的CDATA区域。
- 在真正的XML文档中,可以使用
document.createCDataSection()
来创建CDATA区域。
DocumentType类型
-
DocumentType类型在Web浏览器中并不常用,仅有 Firefox Safari 和 Opera支持它。
-
nodeType
的值为10 -
nodeName
的值为doctype的名称 -
nodeValue
的值是null
-
parentNode
是Document
- 不支持(没有)子节点
-
- 通常,浏览器中的文档使用的都是HTML或XHTML文档类型,只有name属性是有用的。
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
console.log(document.doctype.name); // "HTML"
- IE不支持DocumentType,因此
document.doctype
的值始终都是null
DocumentFragment 类型
- 所有节点类型中,只有DocumentFragment在文档中没有对应的标记。
-
DOM规定文档片段(document fragment)是一种轻量级的文档,可以包含和控制节点,但不会像完整的文档那样占用额外的资源。
-
nodeType
的值为11 -
nodeName
的值为"#document-fragment" -
nodeValue
的值是null
-
parentNode
是null
- 子节点可以是
Element
、ProcessingInstruction
、Comment
、Text
、CDATASection
、EntityReference
-
- 虽然不能把文档文段直接添加到文档中,但可以将它作为一个仓库来使用,在里面保存将来可能会添加到文档中的节点。
-
document.createDocumentFragment()
方法创建文档片段
<ul id="myList"></ul>
var fragment = document.createDocumentFragment();
var ul = document.getElementById("myList");
var li = null;
// 如果直接向ul添加li元素会导致浏览器反复渲染
// fragment作为一个元素中转的仓库避免了这个问题
for (var i=0; i < 3; i++) {
li = document.createElement("li");
li.appendChild(document.createTextNode("Item " + (i+1)));
fragment.appendChild(li);
}
// 这里只会将fragment的所有子节点添加到ul上
// 而fragment本身永远不会成为文档树的一部分
ul.appendChild(fragment);
Attr类型
-
元素的特性在DOM中以Attr类型来表示。在所有浏览器中(包括IE8),都可以访问 Attr类型的构造函数和原型。
-
nodeType
的值为2 -
nodeName
的值就是特性的名称 -
nodeValue
的值就是特性的值 -
parentNode
是null
- HTML中不支持(没有)子节点
- XML中子节点可以是Text或EntityReference
-
- 尽管Attr是节点,但特性却不被认为是DOM文档树的一部分。
- Attr对象有三个属性:
name
value
specified
。 -
document.createAttribute()
传入特性的名称可以创建新的特性节点。
var attr = document.createAttribute("align");
attr.value = "left";
element.setAttribute(attr);
console.log(element.attributes["align"].value); // left
console.log(element.getAttributeNode("align").value); // left
console.log(element.getAttribute("align")); // left
DOM操作技术
动态脚本
- 使用
<script>
元素可以向页面中插入JavaScript代码,一种是通过其src特性包含外部文件,另一种就是用这个元素本身包含代码。 - 动态加载的JavaScript文件能够立即运行。
// 在执行最后一行代码把<script>元素添加到页面中之前
// 是不会下载外部文件的
var script = document.createElement("script");
script.type = "text/javascript";
script.src = "client.js";
document.body.appendChild(script);
<script type="text/javascript" src="client.js"></script>
- 遗憾的是,并没有什么标准方式来探知脚本是否加载完成。
- 从逻辑上讲,使用行内方式直接插入代码是有效的。在Firefox Safari Chrome Opera中,都可以正常运行,但在IE中,会导致错误。IE将
<script>
视为一个特殊元素,不允许DOM访问其子节点。不过可以使用<script>
元素的text属性来制定JavaScript代码
var script = document.createElement("script");
script.type = "text/javascript";
// 这样IE不支持
script.appendChild(
document.createTextNode("function sayHi() { console.log('Hi')}")
);
// 可以使用`<script>`元素的text属性来制定JavaScript代码
script.text = "function sayHi() { console.log('Hi')}";
document.body.appendChild(script);
- Safari3之前的版本不支持这种写法,可以这样做兼容处理
var script = document.createElement("script");
script.type = "text/javascript";
var code = "function sayHi() { console.log('Hi')}"
try {
script.appendChild(document.createTextNode(code));
} catch (ex) {
script.text = code;
}
document.body.appendChild(script);
- 实际上,这样执行代码与在全局作用域中把相同的字符串传递给
eval()
是一样的。
动态样式
- 与动态脚本类似,所谓动态样式是指在页面刚加载时不存在的样式;动态样式是在页面加载完成后动态添加到页面中的。
var link = document.createElement("link");
link.rel = "stylesheet";
link.type = "text/css";
link.href = "style.css";
var head = document.getElementsByTagName("head")[0];
head.appendChild(head);
<link rel="stylesheet" type="text/css" href="styles.css">
- 必须将
<link>
元素添加到<head>
而不是<body>
元素,才能保证所在浏览器中的行为一致。 - 加载外部样式文件的过程是异步的,也就是加载样式 与执行JavaScript代码的过程没有固定的次序。
- 一般是否知道样式已加载完成并不重要,但也存在几种利用事件来检测这个过程是否完成的技术,将在第13章讨论。
- 行内方式插入样式也是可以的,同样要对IE做兼容处理
function loadStyleString(css) {
var style = document.createElement("style");
style.type = "text/css";
try {
style.appendChild(document.createTextNode(css));
} catch (ex) {
style.styleSheet.cssText = css;
}
document.getElementsByTagName("head")[0].appendChild(style);
}
- 如果专门针对IE编写代码,务必小心使用
styleSheet.cssText
属性。在重用同一个<style>
元素并再次设置这个属性时,有可能导致浏览器崩溃。同样将cssText
属性设置为空字符串也可能导致浏览器崩溃。
操作表格
-
<table>
元素是HTML中最复杂的结构之一。想要创建表格,一般都必须涉及表示表格行、单元格、表头等方面。由于涉及的标签多,因而使用核心DOM方法创建和修改表格往往都免不了要编写大量的代码。
<table border="1" width="100%">
<tbody>
<tr>
<td>Cell 1,1</td>
<td>Cell 2,1</td>
</tr>
<tr>
<td>Cell 1,2</td>
<td>Cell 2,2</td>
</tr>
</tbody>
</table>
// 使用核心DOM方法创建这些元素
// 创建table
var table = document.createElement("table");
table.border = 1;
table.width = "100%";
// 创建tbody
var tbody = document.createElement("tbody");
table.appendChild(tbody);
// 创建第一行
var row1 = document.createElement("tr");
tbody.appendChild(row1);
var cell1_1 = document.createElement("td");
cell1_1.appendChild(document.createTextNode("Cell 1,1"));
row1.appendChild(cell1_1);
var cell2_1 = document.createElement("td");
cell2_1.appendChild(document.createTextNode("Cell 2,1"));
row1.appendChild(cell2_1);
// 创建第二行
var row2 = document.createElement("tr");
tbody.appendChild(row2);
var cell1_2 = document.createElement("td");
cell1_2.appendChild(document.createTextNode("Cell 1,2"));
row2.appendChild(cell1_2);
var cell2_2 = document.createElement("td");
cell2_2.appendChild(document.createTextNode("Cell 2,2"));
row2.appendChild(cell2_2);
// 将表格添加到文档主体中
document.body.appendChild(table);
- DOM代码很长,还有点不好理解。为了方便构建表格,HTMLDOM还为
<table>
<tbody>
<tr>
元素添加了一些属性和方法。 -
<table>
元素添加的属性和方法:- caption: 保存着对
<caption>
元素(如果有)的指针 - tBodies: 是一个
<tbody>
元素的HTMLCollction - tFoot: 保存着对
<tfoot>
元素的(如果有)指针 - tHead: 保存着对
<thead>
元素的(如果有)指针 - rows: 是一个表格中所有行的HTMLCollection
- createTHead(): 创建
<thead>
元素,将其放到表格中,返回引用 - createTFoot(): 创建
<tfoot>
元素,将其放到表格中,返回引用 - createCaption(): 创建
<caption>
元素,将其放到表格中,返回引用 - deleteTHead(): 删除
<thead>
元素 - deleteTFoot(): 删除
<tfoot>
元素 - deleteCaption(): 删除
<caption>
元素 - deleteRow(pos): 删除指定位置的行
- insertRow(pos): 向
rows
集合中的指定位置插入一行
- caption: 保存着对
-
为
<tbody>
元素添加的属性和方法如下:- rows: 保存着
<tbody>
元素中行的HTMLCollection - deleteRow(pos): 删除指定位置的行
- insertRow(pos): 向
rows
集合中的指定位置插入一行
- rows: 保存着
-
为
<tr>
元素添加的属性和方法如下:- cells: 保存着
<tr>
元素中单元格的HTMLCollection - deleteCell(pos): 删除指定位置的单元格
- insertCell(pos): 向
cells
集合中的指定位置插入一个单元格,返回对新插入单元格的引用。
- cells: 保存着
// 根据以上属性和方法,可以大大简化前述代码
// 创建table
var table = document.createElement("table");
table.border = 1;
table.width = "100%";
// 创建tbody
var tbody = document.createElement("tbody");
table.appendChild(tbody);
// 创建第一行
tbody.insertRow(0);
tbody.rows[0].insertCell(0);
tbody.rows[0].cells[0].appendChild(document.createTextNode("Cell 1,1"));
tbody.rows[0].insertCell(1);
tbody.rows[0].cells[1].appendChild(document.createTextNode("Cell 2,1"));
// 创建第二行
tbody.insertRow(0);
tbody.rows[1].insertCell(0);
tbody.rows[1].cells[0].appendChild(document.createTextNode("Cell 1,2"));
tbody.rows[1].insertCell(1);
tbody.rows[1].cells[1].appendChild(document.createTextNode("Cell 2,2"));
// 将表格添加到文档主体中
document.body.appendChild(table);
使用NodeList
- 理解
NodeList
及其近亲NamedNodeMap
和HTMLCollection
,是从整体上透彻理解DOM的关键所在。这三个集合都是动态的,每当文档结构发生变化,它们都会得到更新。 - 本质上说,所有
NodeList
对象都是在访问DOM文档实时运行的查询。
// 下列代码会导致无限循环
var divs = document.getElementsByTagName("div");
var div;
// 每次循环都要对条件 i < divs.length 求值
// 但每次循环都添加了一个新的div
for (var i=0; i < divs.length; i++) {
div = document.createElement("div");
document.body.appendChild(div);
}
// 最好使用length属性初始化第二个变量
var divs = document.getElementsByTagName("div");
var i, len, div;
// len保存着第一次循环时div的数量,不会随着循环增加
for (i=0, len=divs.length; i < len; i++) {
div = document.createElement("div");
document.body.appendChild(div);
}
- 尽量减少访问
NodeList
的次数,因为每次访问都会运行一次基于文档的查询。可以考虑将从NodeList
中取得的值缓存起来。 - 理解DOM的关键就是理解DOM对性能的影响。DOM操作往往是JavaScript程序中开销最大的部分。有鉴于此,最好减少DOM操作。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。