js高级程序设计(十)DOM
节点层次
DOM 可以将任何 HTML 或 XML 文档描绘成一个由多层节点构成的结构。节点分为几种不同的类 型,每种类型分别表示文档中不同的信息及(或)标记。
<html> <head> <title>Sample Page</title> </head> <body> <p>Hello World!</p> </body> </html>
文档节点是每个文档的根节点。在这个例子中,文档节点只有一个子节点,即<html>元素,我们称之为文档元素。文档元素是文档的最外层元素,文档中的其他所有元素都包含在文档元素中。每个文档只能有一个文档元素。在 HTML 页面中,文档元素始终都是元素。
每一段标记都可以通过树中的一个节点来表示:HTML 元素通过元素节点表示,特性(attribute) 通过特性节点表示,文档类型通过文档类型节点表示,而注释则通过注释节点表示。总共有 12 种节点 类型,这些类型都继承自一个基类型。
Node类型
DOM1 级定义了一个 Node 接口,该接口将由 DOM 中的所有节点类型实现。这个 Node 接口在 JavaScript 中是作为 Node 类型实现的。
每个节点都有一个 nodeType 属性,用于表明节点的类型。节点类型由在 Node 类型中定义的下列 12 个数值常量来表示,任何节点类型必居其一:
Node.ELEMENT_NODE(1); Node.ATTRIBUTE_NODE(2); Node.TEXT_NODE(3); Node.CDATA_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."); }
为了确保跨浏览器兼容,最好还是将 nodeType 属性与数字值进行比较
if (someNode.nodeType == 1){ //适用于所有浏览器 alert("Node is an element."); }
1.nodeName和nodeValue属性
在使用这两个值以前,最好是像下面这样先检测一下节点的类型。
if (someNode.nodeType == 1){ value = someNode.nodeName; //nodeName 的值是元素的标签名 }
首先检查节点类型,看它是不是一个元素。如果是,则取得并保存 nodeName 的值。 对于元素节点,nodeName 中保存的始终都是元素的标签名,而 nodeValue 的值则始终为 null。
2.节点关系
节点间的各种关系可以用传统的家族关系来描 述,相当于把文档树比喻成家谱。
每个节点都有一个 childNodes 属性,其中保存着一个 NodeList 对象。NodeList 是一种类数组 对象,用于保存一组有序的节点,可以通过位置来访问这些节点。请注意,虽然可以通过方括号语法来 访问 NodeList 的值,而且这个对象也有 length 属性,但它并不是 Array 的实例。NodeList 对象的独特之处在于,它实际上是基于 DOM 结构动态执行查询的结果,因此 DOM 结构的变化能够自动反映在 NodeList 对象中。
访问保存在 NodeList 中的节点——可以通过方括号,也可以使用 item() 方法。
var firstChild = someNode.childNodes[0]; var secondChild = someNode.childNodes.item(1); var count = someNode.childNodes.length;
对 arguments 对象使用 Array.prototype.slice()方法可以 将其转换为数组。而采用同样的方法,也可以将 NodeList 对象转换为数组。
//在 IE8 及之前版本中无效 var arrayOfNodes = Array.prototype.slice.call(someNode.childNodes,0);
要想在 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 列表中 的所有节点都具有相同的父节点,因此它们的 parentNode 属性都指向同一个节点。此外,包含在 childNodes 列表中的每个节点相互之间都是同胞节点。通过使用列表中每个节点的 previousSibling 和 nextSibling 属性,可以访问同一列表中的其他节点。列表中第一个节点的 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."); }
父节点的 firstChild 和 lastChild 属性分别指向其 childNodes 列表中的第一个和最后一个节点。someNode.firstChild 的值 始终等于 someNode.childNodes[0] , 而 someNode.lastChild 的值始终等于 someNode. childNodes [someNode.childNodes.length-1]。在只有一个子节点的情况下,firstChild 和 lastChild 指向同一个节点。如果没有子节点,那么 firstChild 和 lastChild 的值均为 null。
hasChildNodes()也是一个非常有用 的方法,这个方法在节点包含一或多个子节点的情况下返回 true;应该说,这是比查询 childNodes 列表的 length 属性更简单的方法。
所有节点都有的最后一个属性是 ownerDocument,当前元素的document对象,其是当前元素的祖先。
3.操作节点
最常用的方法是 appendChild(),用于向 childNodes 列表的末尾添加一个节点。添加节点后,childNodes 的新增 节点、父节点及以前的最后一个子节点的关系指针都会相应地得到更新。更新完成后,appendChild() 返回新增的节点。
var returnedNode = someNode.appendChild(newNode); alert(returnedNode == newNode); //true alert(someNode.lastChild == newNode); //true
如果传入到 appendChild()中的节点已经是文档的一部分了,那结果就是将该节点从原来的位置 转移到新位置。即使可以将 DOM 树看成是由一系列指针连接起来的,但任何 DOM 节点也不能同时出 现在文档中的多个位置上。因此,如果在调用 appendChild()时传入了父节点的第一个子节点,那么 该节点就会成为父节点的最后一个子节点。
//someNode 有多个子节点 var returnedNode = someNode.appendChild(someNode.firstChild); alert(returnedNode == someNode.firstChild); //false alert(returnedNode == someNode.lastChild); //true
如果需要把节点放在 childNodes 列表中某个特定的位置上,可以使用 insertBefore()方法。这个方法接受两个参数:要插入的节点和作为参照的节点。插入节点后,被插 入的节点会变成参照节点的前一个同胞节点(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);
如果只想移除而非替换节点,可以使用 removeChild()方法。这个方法接受一个参数,即要移除 的节点。被移除的节点将成为方法的返回值。
//移除第一个子节点 var formerFirstChild = someNode.removeChild(someNode.firstChild); //移除最后一个子节点 var formerLastChild = someNode.removeChild(someNode.lastChild);
以上的四个方法都是操作子节点的方法。
4.其他方法
有两个方法是所有类型的节点都有的。
cloneNode(),用于创建调用这个方法的节点的一个完全相同的副本。cloneNode()方法接受一个布尔值参数,表示是否执行深复制。在参数为 true 的情况下,执行深复制,也就是复制节点及其整个子节点树;在参数为 false 的情况下,执行浅复制, 即只复制节点本身。复制后返回的节点副本属于文档所有,但并没有为它指定父节点。因此,这个节点 副本就成为了一个“孤儿”,除非通过 appendChild()、insertBefore()或 replaceChild()将它添加到文档中。
<ul> <li>item 1</li> <li>item 2</li> <li>item 3</li> </ul>
将元素的引用保存在了变量 myList 中
var deepList = myList.cloneNode(true); alert(deepList.childNodes.length); //3(IE < 9)或 7(其他浏览器) var shallowList = myList.cloneNode(false); alert(shallowList.childNodes.length); //0
cloneNode()方法不会复制添加到 DOM 节点中的 JavaScript 属性,例如事件处 理程序等。IE 在此存在一个 bug,即它会复制事件处理程序,所以我们建议在复制 之前最好先移除事件处理程序。
normalize(),这个方法唯一的作用就是处理文档树中的文本节点。 由于解析器的实现或 DOM 操作等原因,可能会出现文本节点不包含文本,或者接连出现两个文本节点 的情况。当在某个节点上调用这个方法时,就会在该节点的后代节点中查找上述两种情况。如果找到了 空文本节点,则删除它;如果找到相邻的文本节点,则将它们合并为一个文本节点。
Document类型
JavaScript 通过 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 对象。通过这个文档对象,不仅可以取得与页面有关的信息,而且还 能操作页面的外观及其底层结构。
1.文档的子节点
Document 节点有两个内置的访问其子节点的快捷方式,第一个就是 documentElement 属性,该属性始终指向 HTML 页面中的元素。另一个就是通过 childNodes 列表访问文档元素, 但通过 documentElement 属性则能更快捷、更直接地访问该元素。
var html = document.documentElement; //取得对<html>的引用 alert(html === document.childNodes[0]); //true alert(html === document.firstChild); //true
作为 HTMLDocument 的实例,document 对象还有一个 body 属性,直接指向元素。
var body = document.body; //取得对<body>的引用
所有浏览器都支持 document.documentElement 和 document.body 属性。
Document 另一个可能的子节点是 DocumentType。通常将标签看成一个与文档其他 部分不同的实体,可以通过 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> <body> </body> </html> <!--第二条注释 -->
看起来这个页面应该有 3 个子节点:注释、元素、注释。现实中的浏览器在处理位于 外部的注释方面存在如下差异。
- IE8 及之前版本、Safari 3.1 及更高版本、Opera 和 Chrome 只为第一条注释创建节点,不为第二 条注释创建节点。结果,第一条注释就会成为 document.childNodes 中的第一个子节点。
- IE9 及更高版本会将第一条注释创建为 document.childNodes 中的一个注释节点,也会将第 二条注释创建为 document.childNodes 中的注释子节点。
- Firefox 以及 Safari 3.1 之前的版本会完全忽略这两条注释。
2.文档信息
document 对象还有一些属性。第一个属性就是 title,包含着<title>元素中的文本。通过这个属性可以取得当前页面的标题,也可以修改当前页面的标题并反映在浏览器的标题栏中。修改 title 属性的值不会改变<title>元素。
//取得文档标题 var originalTitle = document.title; //设置文档标题 document.title = "New page title";
URL、domain 和 referrer。
URL 属性 中包含页面完整的 URL(即地址栏中显示的 URL),domain 属性中只包含页面的域名,而 referrer 属性中则保存着链接到当前页面的那个页面的 URL。在没有来源页面的情况下,referrer 属性中可能 会包含空字符串。所有这些信息都存在于请求的 HTTP 头部,只不过是通过这些属性让我们能够在 JavaScrip 中访问它们而已。
在这 3 个属性中,只有 domain 是可以设置的。但由于安全方面的限制,也并非可以给 domain 设 置任何值。如果 URL 中包含一个子域名,例如 p2p.wrox.com,那么就只能将 domain 设置为"wrox.com" (URL 中包含"www",如 www.wrox.com 时,也是如此)。不能将这个属性设置为 URL 中不包含的域。
//假设页面来自 p2p.wrox.com 域 document.domain = "wrox.com"; // 成功 document.domain = "nczonline.net"; // 出错!
当页面中包含来自其他子域的框架或内嵌框架时,能够设置 document.domain 就非常方便了。由 于跨域安全限制,来自不同子域的页面无法通过 JavaScript 通信。而通过将每个页面的 document.domain 设置为相同的值,这些页面就可以互相访问对方包含的 JavaScript 对象了。例如, 假设有一个页面加载自 www.wrox.com,其中包含一个内嵌框架,框架内的页面加载自 p2p.wrox.com。 由于 document.domain 字符串不一样,内外两个页面之间无法相互访问对方的 JavaScript 对象。但如 果将这两个页面的 document.domain 值都设置为"wrox.com",它们之间就可以通信了。
浏览器对 domain 属性还有一个限制,即如果域名一开始是“松散的”(loose),那么不能将它再设 置为“紧绷的”(tight)。换句话说,在将 document.domain 设置为"wrox.com"之后,就不能再将其 设置回"p2p.wrox.com",否则将会导致错误。
//假设页面来自于 p2p.wrox.com 域 document.domain = "wrox.com"; //松散的(成功) document.domain = "p2p.wrox.com"; //紧绷的(出错!)
3.查找元素
Document 类型为此提供了两个方法:getElementById()和getElementsByTagName()。
getElementById(),接收一个参数:要取得的元素的ID。如果找到相应的元素则返回该元素,如果不存在带有相应ID 的元素,则返回null。
<div id="myDiv">Some text</div> var div = document.getElementById("myDiv"); //取得<div>元素的引用
getElementsByTagName(),这个方法接受一个参数,即要取得元素的标签名,而返回的是包含零或多个元素的NodeList。在HTML 文档中,这个方法会返回一个HTMLCollection 对象,作为一个“动态”集合,该对象与NodeList 非常类似。
var images = document.getElementsByTagName("img");
与NodeList 对象类似,可以使用方括号语法或item()方法来访问HTMLCollection 对象中的项。而这个对象中元素的数量则可以通过其length 属性取得。
alert(images.length); //输出图像的数量 alert(images[0].src); //输出第一个图像元素的src 特性 aler t(images.item(0).src); //输出第一个图像元素的 src 特性
HTMLCollection 对象还有一个方法,叫做namedItem(),使用这个方法可以通过元素的name特性取得集合中的项。
<img src="myimage.gif" name="myImage">
var myImage = images.namedItem("myImage");
在提供按索引访问项的基础上,HTMLCollection 还支持按名称访问项,这就为我们取得实际想要的元素提供了便利。而且,对命名的项也可以使用方括号语法来访问。
var myImage = images["myImage"];
要想取得文档中的所有元素,可以向getElementsByTagName()中传入"*"。
var allElements = document.getElementsByTagName("*");
getElementsByName()。顾名思义,这个方法会返回带有给定name 特性的所有元素。
与getElementsByTagName()类似,getElementsByName()方法也会返回一个HTMLCollectioin。
4.特殊集合
- document.anchors,包含文档中所有带name 特性的<a>元素;
-
document.forms,包含文档中所有的<form>元素,与document.getElementsByTagName("form")得到的结果相同;
-
document.images,包含文档中所有的<img>元素,与document.getElementsByTagName("img")得到的结果相同;
- document.links,包含文档中所有带href 特性的<a>元素。
6.DOM一致性检测
由于DOM 分为多个级别,也包含多个部分,因此检测浏览器实现了DOM的哪些部分就十分必要了。document.implementation 属性就是为此提供相应信息和功能的对象,与浏览器对DOM的实现直接对应。DOM1 级只为document.implementation 规定了一个方法,即hasFeature()。这个方法接受两个参数:要检测的DOM 功能的名称及版本号。如果浏览器支持给定名称和版本的功能,则该方法返回true。
var hasXmlDom = document.implementation.hasFeature("XML", "1.0");
在使用DOM 的某些特殊的功能之前,最好除了检测hasFeature()之外,还同时使用能力检测。
6.文档写入
write()、writeln()、open()和close()。其中,write()和writeln()方法都接受一个字符串参数,即要写入到输出流中的文本。write()会原样写入,而writeln()则会在字符串的末尾添加一个换行符(\n)。在页面被加载的过程中,可以使用这两个方法向页面中动态地加入内容。
<html> <head> <title>document.write() Example</title> </head> <body> <p>The current date and time is: <script type="text/javascript"> document.write("<strong>" + (new Date()).toString() + "</strong>"); </script> </p> </body> </html>
在页面加载过程中输出当前日期和时间的代码。这样做会创建一个DOM 元素,而且可以在将来访问该元素。通过write()和writeln()输出的任何HTML 代码都将如此处理。
方法open()和close()分别用于打开和关闭网页的输出流。
Element类型
Element 类型用 于表现 XML 或 HTML元素,提供了对元素标签名、子节点及特性的访问。
- nodeType 的值为 1;
- nodeName 的值为元素的标签名;
- nodeValue 的值为 null;
- parentNode 可能是 Document 或 Element;
- 其子节点可能是 Element、Text、Comment、ProcessingInstruction、CDATASection 或 EntityReference。
要访问元素的标签名,可以使用 nodeName 属性,也可以使用 tagName 属性;这两个属性会返回 相同的值。
<div id="myDiv"></div> var div = document.getElementById("myDiv"); alert(div.tagName); //"DIV" alert(div.tagName == div.nodeName); //true
div.tagName 实际上输出的是 "DIV"而非"div"。在 HTML 中,标签名始终都以全部大写表示;而在 XML(有时候也包括 XHTML) 中,标签名则始终会与源代码中的保持一致。假如你不确定自己的脚本将会在 HTML 还是 XML 文档中 执行,最好是在比较之前将标签名转换为相同的大小写形式。
if (element.tagName == "div"){ //不能这样比较,很容易出错! //在此执行某些操作 } if (element.tagName.toLowerCase() == "div"){ //这样最好(适用于任何文档) //在此执行某些操作 }
1.HTML元素
所有 HTML 元素都由 HTMLElement 类型表示,不是直接通过这个类型,也是通过它的子类型来表 示。HTMLElement 类型直接继承自Element 并添加了一些属性。添加的这些属性分别对应于每个 HTML 元素中都存在的下列标准特性。
- id,元素在文档中的唯一标识符。
- title,有关元素的附加说明信息,一般通过工具提示条显示出来。
- lang,元素内容的语言代码,很少使用。
- dir,语言的方向,值为"ltr"(left-to-right,从左至右)或"rtl"(right-to-left,从右至左), 也很少使用。
- className,与元素的class 特性对应,即为元素指定的CSS类。没有将这个属性命名为class, 是因为 class 是 ECMAScript 的保留字
<div id="myDiv" class="bd" title="Body text" lang="en" dir="ltr"></div> var div = document.getElementById("myDiv"); alert(div.id); //"myDiv"" alert(div.className); //"bd" alert(div.title); //"Body text" alert(div.lang); //"en" alert(div.dir); //"ltr"
前面提到过,所有 HTML 元素都是由 HTMLElement 或者其更具体的子类型来表示的。下表列出了 所有 HTML 元素以及与之关联的类型(以斜体印刷的元素表示已经不推荐使用了)。
2.取得特性
操作特性的 DOM 方法主要有三个,分别是 getAttribute()、setAttribute()和 removeAttribute()。
var div = document.getElementById("myDiv"); alert(div.getAttribute("id")); //"myDiv" alert(div.getAttribute("class")); //"bd" alert(div.getAttribute("title")); //"Body text" alert(div.getAttribute("lang")); //"en" alert(div.getAttribute("dir")); //"ltr"
传递给 getAttribute()的特性名与实际的特性名相同。因此要想得到 class 特性值,应 该传入"class"而不是"className",后者只有在通过对象属性访问特性时才用。如果给定名称的特性 不存在,getAttribute()返回 null。
通过 getAttribute()方法也可以取得自定义特性。
有两类特殊的特性,它们虽然有对应的属性名,但属性的值与通过 getAttribute()返回的值并不 相同。第一类特性就是 style,用于通过 CSS 为元素指定样式。在通过 getAttribute()访问时,返 回的 style 特性值中包含的是 CSS 文本,而通过属性来访问它则会返回一个对象。第二类与众不同的特性是 onclick 这样的事件处理程序。当在元素上使用时,onclick 特性中包 含的是 JavaScript 代码,如果通过 getAttribute()访问,则会返回相应代码的字符串。而在访问 onclick 属性时,则会返回一个 JavaScript 函数。
3.设置特性
与 getAttribute()对应的方法是 setAttribute(),这个方法接受两个参数:要设置的特性名和 值。如果特性已经存在,setAttribute()会以指定的值替换现有的值;如果特性不存在,setAttribute() 则创建该属性并设置相应的值。
div.setAttribute("id", "someOtherId"); div.setAttribute("class", "ft"); div.setAttribute("title", "Some other text"); div.setAttribute("lang","fr"); div.setAttribute("dir", "rtl");
用setAttribute()为 DOM 元素添加一个自定义的属性,该属性不会自动成为元素的特性。只有一开始写在DOM元素里的自定义属性才能通过DOM元素的属性来访问。
removeAttribute(),这个方法用于彻底删除元素的特性。调用这个方 法不仅会清除特性的值,而且也会从元素中完全删除特性。
- DOM对象初始化时会在创建默认的基本property;
- 只有在HTML标签中定义的attribute才会被保存在property的attributes属性中;
- attribute会初始化property中的同名属性,但自定义的attribute不会出现在property中;
- attribute的值都是字符串;
4.attributes属性
Element 类型是使用attributes 属性的唯一一个DOM 节点类型。attributes 属性中包含一个NamedNodeMap,与NodeList 类似,也是一个“动态”的集合。元素的每一个特性都由一个Attr 节点表示,每个节点都保存在NamedNodeMap 对象中。NamedNodeMap 对象拥有下列方法。
- getNamedItem(name):返回nodeName 属性等于name 的节点
- removeNamedItem(name):从列表中移除nodeName 属性等于name 的节点
- setNamedItem(node):向列表中添加节点,以节点的nodeName 属性为索引
- item(pos):返回位于数字pos 位置处的节点
attributes 属性中包含一系列节点,每个节点的nodeName 就是特性的名称,而节点的nodeValue就是特性的值。
var id = element.attributes.getNamedItem("id").nodeValue; //获取元素id特性 var id = element.attributes["id"].nodeValue;
也可以使用这种语法来设置特性的值,即先取得特性节点,然后再将其nodeValue 设置为新值
element.attributes["id"].nodeValue = "someOtherId";
removeNamedItem()直接删除具有给定名称的特性,返回表示被删除特性的Attr 节点。
var oldAttr = element.attributes.removeNamedItem("id");
遍历元素特性保存成字符串
function outputAttributes(element){ var pairs = new Array(), attrName, attrValue, i, len; for (i=0, len=element.attributes.length; i < len; i++){ attrName = element.attributes[i].nodeName; attrValue = element.attributes[i].nodeValue; pairs.push(attrName + "=\"" + attrValue + "\""); } return pairs.join(" "); }
5.创建元素
使用document.createElement()方法可以创建新元素。这个方法只接受一个参数,即要创建元素的标签名。
var div = document.createElement("div");
在使用createElement()方法创建新元素的同时,也为新元素设置了ownerDocuemnt 属性。此时,还可以操作元素的特性,为它添加更多子节点,以及执行其他操作。
div.id = "myNewDiv";
div.className = "box";
在新元素上设置这些特性只是给它们赋予了相应的信息。由于新元素尚未被添加到文档树中,因此设置这些特性不会影响浏览器的显示。要把新元素添加到文档树,可以使用appendChild()、insertBefore()或replaceChild()方法。
一旦将元素添加到文档树中,浏览器就会立即呈现该元素。此后,对这个元素所作的任何修改都会实时反映在浏览器中。
在IE7中创建元素最好使用完整的HTML标签来解决,这样可以避免很多IE7bug。辣鸡IE,真辣鸡。
6.元素的子节点
元素可以有任意数目的子节点和后代节点,因为元素可以是其他元素的子节点。元素的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>
对于这段代码,<ul>元素在任何浏览器中都会包含3 个子节点。如果需要通过childNodes 属性遍历子节点,那么一定不要忘记浏览器间的这一差别。这意味着在执行某项操作以前,通常都要先检查一下nodeTpye 属性。
for (var i=0, len=element.childNodes.length; i < len; i++){ if (element.childNodes[i].nodeType == 1){ //执行某些操作 } }
这个例子会循环遍历特定元素的每一个子节点,然后只在子节点的nodeType 等于1(表示是元素节点)的情况下,才会执行某些操作。
元素也支持getElementsByTagName()方法。
var ul = document.getElementById("myList"); var items = ul.getElementsByTagName("li");
这里<ul>的后代中只包含直接子元素。不过,如果它包含更多层次的后代元素,那么各个层次中包含的<li>元素也都会返回。
Text类型
文本节点由Text 类型表示,包含的是可以照字面解释的纯文本内容。纯文本中可以包含转义后的HTML 字符,但不能包含HTML 代码。
- nodeType 的值为3;
- nodeName 的值为"#text";
- nodeValue 的值为节点所包含的文本;
- parentNode 是一个Element;
- 不支持(没有)子节点。
可以通过nodeValue 属性或data 属性访问Text 节点中包含的文本,这两个属性中包含的值相同。对nodeValue 的修改也会通过data 反映出来,反之亦然。使用下列方法可以操作节点中的文本。
- appendData(text):将text 添加到节点的末尾。
- deleteData(offset, count):从offset 指定的位置开始删除count 个字符。
- 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>
在修改文本节点时还要注意,此时的字符串会经过HTML(或XML,取决于文档类型)编码。
//输出结果是"Some <strong>other</strong> message" div.firstChild.nodeValue = "Some <strong>other</strong> message";
1.创建文本节点
document.createTextNode()创建新文本节点,这个方法接受一个参数——要插入节点中的文本。与设置已有文本节点的值一样,作为参数的文本也将按照HTML 或XML 的格式进行编码。
var textNode = document.createTextNode("<strong>Hello</strong> world!");
在创建新文本节点的同时,也会为其设置ownerDocument 属性。不过,除非把新节点添加到文档树中已经存在的节点中,否则我们不会在浏览器窗口中看到新节点。下面的代码会创建一个<div>元素并向其中添加一条消息。
var element = document.createElement("div"); element.className = "message"; var textNode = document.createTextNode("Hello world!"); element.appendChild(textNode); document.body.appendChild(element);
一般情况下,每个元素只有一个文本子节点。不过,在某些情况下也可能包含多个文本子节点。
var element = document.createElement("div"); element.className = "message"; var textNode = document.createTextNode("Hello world!"); element.appendChild(textNode); var anotherTextNode = document.createTextNode("Yippee!"); element.appendChild(anotherTextNode); document.body.appendChild(element);
如果两个文本节点是相邻的同胞节点,那么这两个节点中的文本就会连起来显示,中间不会有空格。
2.规范化文本节点
DOM 文档中存在相邻的同胞文本节点很容易导致混乱,因为分不清哪个文本节点表示哪个字符串。另外,DOM 文档中出现相邻文本节点的情况也不在少数,于是就催生了一个能够将相邻文本节点合并的方法。这个方法是由Node 类型定义的(因而在所有节点类型中都存在),名叫normalize()。如果在一个包含两个或多个文本节点的父元素上调用normalize()方法,则会将所有文本节点合并成一个节点,结果节点的nodeValue 等于将合并前每个文本节点的nodeValue 值拼接起来的值。
var element = document.createElement("div"); element.className = "message"; var textNode = document.createTextNode("Hello world!"); element.appendChild(textNode); var anotherTextNode = document.createTextNode("Yippee!"); element.appendChild(anotherTextNode); document.body.appendChild(element); alert(element.childNodes.length); //2 element.normalize(); alert(element.childNodes.length); //1 alert(element.firstChild.nodeValue); // "Hello world!Yippee!"
浏览器在解析文档时永远不会创建相邻的文本节点。这种情况只会作为执行DOM操作的结果出现。
3.分割文本节点
Text 类型提供了一个作用与normalize()相反的方法:splitText()。这个方法会将一个文本节点分成两个文本节点,即按照指定的位置分割nodeValue 值。原来的文本节点将包含从开始到指定位置之前的内容,新文本节点将包含剩下的文本。这个方法会返回一个新文本节点,该节点与原节点的parentNode 相同。
var element = document.createElement("div"); element.className = "message"; var textNode = document.createTextNode("Hello world!"); element.appendChild(textNode); document.body.appendChild(element); var newNode = element.firstChild.splitText(5); alert(element.firstChild.nodeValue); //"Hello" alert(newNode.nodeValue); //" world!" alert(element.childNodes.length); //2
Comment类型
注释在DOM中是通过Comment 类型来表示的。
- nodeType 的值为8;
- nodeName 的值为"#comment";
- nodeValue 的值是注释的内容;
- parentNode 可能是Document 或Element;
- 不支持(没有)子节点。
Comment 类型与Text 类型继承自相同的基类,因此它拥有除splitText()之外的所有字符串操作方法。与Text 类型相似,也可以通过nodeValue 或data 属性来取得注释的内容。
<div id="myDiv"><!--A comment --></div> var div = document.getElementById("myDiv"); var comment = div.firstChild; alert(comment.data); //"A comment"
使用document.createComment()并为其传递注释文本也可以创建注释节点
var comment = document.createComment("A comment ");
CDATASection类型
CDATASection 类型只针对基于XML 的文档,表示的是CDATA 区域。与Comment 类似,CDATASection 类型继承自Text 类型,因此拥有除splitText()之外的所有字符串操作方法。
- nodeType 的值为4;
- nodeName 的值为"#cdata-section";
- nodeValue 的值是CDATA 区域中的内容;
- parentNode 可能是Document 或Element;
- 不支持(没有)子节点。
CDATA 区域只会出现在XML 文档中,因此多数浏览器都会把CDATA 区域错误地解析为Comment或Element。
在真正的XML 文档中,可以使用document.createCDataSection()来创建CDATA 区域,只需为其传入节点的内容即可。
DocumentType类型
DocumentType 类型在Web 浏览器中并不常用,仅有Firefox、Safari 和Opera 支持它①。Document-Type 包含着与文档的doctype 有关的所有信息。
- nodeType 的值为10;
- nodeName 的值为doctype 的名称;
- nodeValue 的值为null;
- parentNode 是Document;
- 不支持(没有)子节点。
在DOM1 级中,DocumentType 对象不能动态创建,而只能通过解析文档代码的方式来创建。支持它的浏览器会把DocumentType 对象保存在document.doctype 中。DOM1 级描述了DocumentType 对象的3 个属性:name、entities 和notations。其中,name 表示文档类型的名称;entities 是由文档类型描述的实体的NamedNodeMap 对象;notations 是由文档类型描述的符号的NamedNodeMap 对象。通常,浏览器中的文档使用的都是HTML 或XHTML 文档类型,因而entities和notations 都是空列表(列表中的项来自行内文档类型声明)。但不管怎样,只有name 属性是有用的。这个属性中保存的是文档类型的名称,也就是出现在<!DOCTYPE 之后的文本。
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> alert(document.doctype.name); //"HTML"
DocumentFragment类型
在所有节点类型中,只有DocumentFragment 在文档中没有对应的标记。DOM 规定文档片段(document fragment)是一种“轻量级”的文档,可以包含和控制节点,但不会像完整的文档那样占用额外的资源。
- nodeType 的值为11;
- nodeName 的值为"#document-fragment";
- nodeValue 的值为null;
- parentNode 的值为null;
-
子节点可以是Element、ProcessingInstruction、Comment、Text、CDATASection 或EntityReference。
虽然不能把文档片段直接添加到文档中,但可以将它作为一个“仓库”来使用,即可以在里面保存将来可能会添加到文档中的节点。要创建文档片段,可以使用document.createDocumentFragment()方法。
var fragment = document.createDocumentFragment();
文档片段继承了Node 的所有方法,通常用于执行那些针对文档的DOM操作。如果将文档中的节点添加到文档片段中,就会从文档树中移除该节点,也不会从浏览器中再看到该节点。添加到文档片段中的新节点同样也不属于文档树。可以通过appendChild()或insertBefore()将文档片段中内容添加到文档中。在将文档片段作为参数传递给这两个方法时,实际上只会将文档片段的所有子节点添加到相应位置上;文档片段本身永远不会成为文档树的一部分。
<ul id="myList"></ul> var fragment = document.createDocumentFragment(); var ul = document.getElementById("myList"); var li = null; for (var i=0; i < 3; i++){ li = document.createElement("li"); li.appendChild(document.createTextNode("Item " + (i+1))); fragment.appendChild(li); } ul.appendChild(fragment);
如果逐个地添加列表项,将会导致浏览器反复渲染(呈现)新信息。为避免这个问题,可以像下面这样使用一个文档片段来保存创建的列表项,然后再一次性将它们添加到文档中。
Attr类型
元素的特性在DOM 中以Attr 类型来表示。在所有浏览器中(包括IE8),都可以访问Attr 类型的构造函数和原型。从技术角度讲,特性就是存在于元素的attributes 属性中的节点。
- nodeType 的值为2;
- nodeName 的值是特性的名称;
- nodeValue 的值是特性的值;
- parentNode 的值为null;
- 在HTML 中不支持(没有)子节点;
- 在XML 中子节点可以是Text 或EntityReference。
尽管它们也是节点,但特性却不被认为是DOM 文档树的一部分。开发人员最常使用的是getAttribute()、setAttribute()和remveAttribute()方法,很少直接引用特性节点。
Attr 对象有3 个属性:name、value 和specified。其中,name 是特性名称(与nodeName 的值相同),value 是特性的值(与nodeValue 的值相同),而specified 是一个布尔值,用以区别特性是在代码中指定的,还是默认的。
var attr = document.createAttribute("align"); attr.value = "left"; element.setAttributeNode(attr); alert(element.attributes["align"].value); //"left" alert(element.getAttributeNode("align").value); //"left" alert(element.getAttribute("align")); //"left"
DOM操作技术
动态脚本
在页面加载时不存在,但将来的某一时刻通过修改DOM 动态添加的脚本。
动态加载的外部JavaScript 文件能够立即运行
var script = document.createElement("script"); script.type = "text/javascript"; script.src = "client.js"; document.body.appendChild(script);
另一种指定JavaScript 代码的方式是行内方式
var script = document.createElement("script"); script.type = "text/javascript"; script.appendChild(document.createTextNode("function sayHi(){alert('hi');}")); document.body.appendChild(script);
在Firefox、Safari、Chrome 和Opera 中,这些DOM 代码可以正常运行。但在IE 中,则会导致错误。IE 将<script>视为一个特殊的元素,不允许DOM 访问其子节点。不过,可以使用<script>元素的text 属性来指定JavaScript 代码。
var script = document.createElement("script"); script.type = "text/javascript"; script.text = "function sayHi(){alert('hi');}"; document.body.appendChild(script);
以下是兼容写法:
var script = document.createElement("script"); script.type = "text/javascript"; var code = "function sayHi(){alert('hi');}"; try { script.appendChild(document.createTextNode("code")); } catch (ex){ script.text = "code"; } document.body.appendChild(script);
实际上,这样执行代码与在全局作用域中把相同的字符串传递给eval()是一样的。
动态样式
<link>元素用于包含来自外部的文件
var link = document.createElement("link"); link.rel = "stylesheet"; link.type = "text/css"; link.href = "style.css"; var head = document.getElementsByTagName("head")[0]; head.appendChild(link);
加载外部样式文件的过程是异步的,也就是加载样式与执行JavaScript 代码的过程没有固定的次序。一般来说,知不知道样式已经加载完成并不重要。
另一种定义样式的方式是使用<style>元素来包含嵌入式CSS
var style = document.createElement("style"); style.type = "text/css"; style.appendChild(document.createTextNode("body{background-color:red}")); var head = document.getElementsByTagName("head")[0]; head.appendChild(style);
以上代码可以在Firefox、Safari、Chrome 和Opera 中运行,在IE 中则会报错。IE 将<style>视为一个特殊的、与<script>类似的节点,不允许访问其子节点。事实上,IE 此时抛出的错误与向<script>元素添加子节点时抛出的错误相同。解决IE 中这个问题的办法,就是访问元素的styleSheet 属性,该属性又有一个cssText 属性,可以接受CSS 代码。
以下是兼容写法
var style = document.createElement("style"); style.type = "text/css"; try{ style.appendChild(document.createTextNode("body{background-color:red}")); } catch (ex){ style.styleSheet.cssText = "body{background-color:red}"; } var head = document.getElementsByTagName("head")[0]; head.appendChild(style);
操作表格
为了方便构建表格,HTML DOM 还为<table>、<tbody>和<tr>元素添加了一些属性和方法。
为<table>元素添加的属性和方法如下。
- caption:保存着对<caption>元素(如果有)的指针。
- tBodies:是一个<tbody>元素的HTMLCollection。
- tFoot:保存着对<tfoot>元素(如果有)的指针。
- tHead:保存着对<thead>元素(如果有)的指针。
- rows:是一个表格中所有行的HTMLCollection。
- createTHead():创建<thead>元素,将其放到表格中,返回引用。
- createTFoot():创建<tfoot>元素,将其放到表格中,返回引用。
- createCaption():创建<caption>元素,将其放到表格中,返回引用。
- deleteTHead():删除<thead>元素。
- deleteTFoot():删除<tfoot>元素。
- deleteCaption():删除<caption>元素。
- deleteRow(pos):删除指定位置的行。
- insertRow(pos):向rows 集合中的指定位置插入一行。
为<tbody>元素添加的属性和方法如下。
- rows:保存着<tbody>元素中行的HTMLCollection。
- deleteRow(pos):删除指定位置的行。
- insertRow(pos):向rows 集合中的指定位置插入一行,返回对新插入行的引用。
为<tr>元素添加的属性和方法如下。
- cells:保存着<tr>元素中单元格的HTMLCollection。
- deleteCell(pos):删除指定位置的单元格。
- insertCell(pos):向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(1); 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"), i, div; for (i=0; i < divs.length; i++){ div = document.createElement("div"); document.body.appendChild(div); }
第一行代码会取得文档中所有<div>元素的HTMLCollection。由于这个集合是“动态的”,因此只要有新<div>元素被添加到页面中,这个元素也会被添加到该集合中。浏览器不会将创建的所有集合都保存在一个列表中,而是在下一次访问集合时再更新集合。结果,在遇到上例中所示的循环代码时,就会导致一个有趣的问题。每次循环都要对条件i < divs.length 求值,意味着会运行取得所有<div>元素的查询。考虑到循环体每次都会创建一个新<div>元素并将其添加到文档中,因此divs.length 的值在每次循环后都会递增。既然i 和divs.length 每次都会同时递增,结果它们的值永远也不会相等。
如果想要迭代一个NodeList,最好是使用length 属性初始化第二个变量,然后将迭代器与该变量进行比较
var divs = document.getElementsByTagName("div"), i, len, div; for (i=0, len=divs.length; i < len; i++){ div = document.createElement("div"); document.body.appendChild(div); }
应该尽量减少访问NodeList 的次数。因为每次访问NodeList,都会运行一次基于文档的查询。所以,可以考虑将从NodeList 中取得的值缓存起来。