本章内容
- 理解包含不同层次节点的 DOM
- 使用不同的节点类型
- 克服浏览器兼容性问题及各种陷阱
DOM 是针对 HTML 和 XML 文档的一个 API。DOM 描绘了一个层次化的节点树,允许开发人员添加、移除和修改页面的某一部分。本章主要讨论与浏览器中的 HTML 页面相关的 DOM1 级的特性和应用,以及 JavaScript 对 DOM1 级的实现。
注意,IE 中的所有 DOM 对象都是以 COM 对象的形式实现的。这意味着 IE 中的 DOM 对象与原生 JavaScript 对象的行为活动特点并不一致。
10.1 节点层次
DOM 可以将任何 HTML 或 XML 文档描绘成一个由多层节点构成的结构。
10.1.1 Node 类型
除了 IE 之外,在其他所有浏览器中都可以访问到这个类型。 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) {
alert("Node is an element");
}
由于 IE 没有公开Node
类型的构造函数,因此上面的代码在 IE 中会导致错误。为确保跨浏览器兼容,最好还是将nodeType
属性与数字值进行比较:
if (someNode.nodeType == 1) {
alert("Node is an element");
}
并非所有节点类型都受到 Web 浏览器的支持。开发人员最常用的就是元素和文本节点。
- nodeName 和 nodeValue 属性
在使用这俩属性之前,最好先检测一下节点的类型。
if (someNode.nodeType == 1) {
name = someNode.nodeName;
}
对于元素节点,nodeName
中保存的始终都是元素的标签名,而nodeValue
的值则始终未null
。
- 节点关系
每个节点都有一个childNodes
属性,其中保存着一个NodeList
对象。它是一种类数组对象,用于保存一组有序的节点,可以通过位置来访问这些节点。它实际上是基于 DOM 结构动态执行查询的结果,因此 DOM 结构的变化能够自动反映在NodeList
对象中。
var firstChild = someNode.childNodes[0];
var secondChild = someNode.childNodes.item(1);
var count = someNode.childNodes.length;
其中,length
属性表示的是访问NodeList
的那一刻,其中包含的节点数量。对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);
} catch(ex) {
array = new Array();
for (var i=0,len=nodes.length;i<len;i++) {
array.push(nodes[i]);
}
}
}
每个节点都有一个parentNode
属性,该属性指向文档树中的树节点。包含在childNodes
列表中的所有节点都具有相同的父节点,因此它们的parentNode
属性都指向同一个节点。此外,包含在childNodes
列表中的每个节点相互之间都是同胞节点。可以使用previousSibling
和nextSibling
属性,可以访问同一列表中的其他节点。
if (someNode.nextSibling === null){
alert("Last node in the parent's childNodes list");
} else if (someNode.previousSibling === null) {
alert("First node in the parent childNodes list.");
}
父节点的firstChild
和lastChild
属性分别子节点列表中的第一个和最后一个节点。
另外,hasChildNodes()
也是一个非常有用的方法,这个方法在节点包含一或多个子节点的情况下返回true
。
所有节点都有的最后一个属性是ownerDocument
,该属性指向表示整个文档的文档节点。这种关系表示的任何节点都属于它所在的文档,任何节点都不能同时存在于两个或更多个文档中。
虽然所有节点类型都继承自
Node
,但并不是每种节点都有子节点。
- 操作节点
其中,最常用的方法是appendChild()
,用于向childNodes
列表的末尾添加一个节点。更新完成后,appendChild()
返回新增的节点。
var returnedNode = someNode.appendChild(newNode);
alert(returnedNode == newNode); //true
alert(someNode.lastChild == newNode); //true
如果传入到appendChild()
中的节点已经是文档中一部分了,那结果就是将该节点从原来的位置转移到新位置。
如果需要把节点放在列表中的某个位置,而不是放在末尾,可以使用insertBefore()
方法。这个方法接受两个参数:要插入的节点和作为参照的节点。如果参照节点是null
,则放在末尾。
//插入后称为最后一个字节点
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
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);
与使用replaceChild()
方法一样,通过removeChild()
移除的节点仍然为文档所有,只不过在文档中已经没有了自己的位置。
要使用这几个方法必须先取得父节点(使用parentNode
属性)。另,并不是所有类型的节点都有子节点。
- 其他方法
有两个方法是所有类型的节点都有的。第一个就是cloneNode()
,用于创建调用这个方法的节点的一个完全相同的副本。cloneNode()
方法接受一个布尔值参数,表示是否执行深复制。参数为true
的情况下,执行深复制,也就是复制节点及其整个子节点树;参数为false
的情况下,执行浅复制,即只复制节点本身。复制后返回的节点副本属于文档所有,但并没有为它指定父节点。因此,这个节点副本就成为了一个“孤儿”,除非通过appendChild()
、insertBefore()
或replaceChild()
将它添加到文档中。
cloneNode()
方法不会复制添加到 DOM 节点中的 JavaScript 属性,例如事件处理程序等。IE 在此存在一个 bug,即它会复制事件处理程序,所以我们建议在复制之前最好先移除事件处理程序。
介绍的最后一个方法是normalize()
,这个方法唯一的作用就是处理文档树中的文本节点。当在某个节点上调用这个方法时,如果找到了空文本节点,则删除它;如果找到相邻的文本节点,则将它们合并为一个文本节点。
10.1.2 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
对象。通过这个文档对象,不仅可以取得与页面有关的信息,而且还能操作页面的外观及其底层结构。
- 文档的子节点
有两个内置的访问其子节点的快捷方式。第一个就是documentElement
属性,该属性始终指向 HTML 页面中的<html>
元素。另一个就是通过childNodes
列表访问文档元素,但通过documentElement
属性则能更快捷、更直接地访问该元素。如下所示:
var html = document.documentElement;
alert(html === document.childNodes[0]); //true(false ?)
alert(html === document.firstChild); //true (false ?)
作为HTMLDocument
的实例,document
对象还有一个body
属性,直接指向<body>
元素。开发人员经常要使用这个元素。
var body = document.body; //取得引用
所有浏览器都支持document.documentElement
和document.body
属性。
另一个可能的子节点是DocumentType
。通常将<!DOCTYPE>
标签看成一个与文档其他部分不同的实体,可以通过doctype
属性来访问它的信息。
var doctype = document.doctype;
由于浏览器对document.doctype
的支持不一致,因此这个属性的用处很有限。
从技术上说,出现在<html>
元素外的注释应该算是文档的子节点。然而,不同的浏览器在是否解析这些注释以及能否正确处理它们等方面,也存在很大差异。
- 文档信息
作为HTMLDocument
的一个实例,document
对象还有一些标准的Document
对象所没有的属性。这些属性提供一些网页的一些信息。
//取得文档标题
var originalTitle = document.title;
//设置文档标题
document.title = "New page title";
//取得完整的 URL
var url = document.URL;
//取得域名
var domain = document.domain;
//取得来源页面的 URL
var referrer = document.referrer;
只有domain
是可以设置的。如果 URL 中包含一个子域名,例如 p2p.wrox.com
,那么就只能将domain
设置为"wrox.com"
(URL 中包含“www”,如 www.wrox.com 时,也是如此)。不能将这个属性设置为 URL 中不包含的域。
当页面中包含来自其他子域的框架或内嵌框架时,能够设置document.domain
就非常方便了。由于跨域安全限制,来自不同子域的页面无法通过 JavaScript 通信。而通过将每个页面的document.domain
设置为相同的值,这些页面就可以互相访问对方包含的JavaScript
对象了。
- 查找元素
第一个方法,getElementById
,通过 ID 获取该元素。
另一个常用于取得元素引用的方法是getElementsByTagName
,通过标签名取得元素,返回的是包含零或多个元素的NodeList
。在 HTML 文档中,这个方法会返回一个 HTMLCollection 对象。
var images = document.getElementByTagName('img');
alert(images.length);
alert(images[0].src);
alert(images.item(0).src);
HTMLCollection
对象还有一个方法,namedItem()
,使用这个方法可以通过元素的name
特性取得集合中的项。
var myImage = images.namedItem("myImage");
还支持按名称访问项。
var myImage = images["myImage"];
对 HTMLCollection
而言,我们可以向方括号中传入数值或字符串形式的索引值。在后台,对数值索引就会调用item()
,而对字符串索引就会调用namedItem()
。
要想取得文档中的所有元素,可以向getElementsByTagName()
中传入"*"
。在 JavaScript 及 CSS 中,星号(*)通常表示“全部”。
var allElements = document.getElementsByTagName("*");
第三个方法,也是只有HTMLDocument
类型才有的方法,是getElementsByName()
。
- 特殊集合
还有一些特殊的集合,这些集合都是HTMLCollection
对象,为访问文档常用的部分提供了快捷方式。 - DOM 一致性检测
由于 DOM 分为多个级别,也包含多个部分,因此检测浏览器实现了 DOM 的哪些部分就十分必要。DOM1 级只为document.implementation
规定了一个方法,即hasFeatrue()
。
var hasXmlDom = document.implementation.hasFeature("XML", "3.0");
建议在多数情况下,在使用 DOM 的某些特殊功能之前,最好除了检测hasFeature()
之外,还同时使用能力检测。
- 文档写入
这个能力体现在下列 4 个方法中:write()
、writeln()
、open()
、close()
。writeln()
会在字符串的末尾添加一个换行符(\n)。
方法open
和close()
分别用于打开和关闭网页的输出流。
严格型 XHTML 文档不支持文档写入。对于那些按照 application/xml + xhtml 内容类型提供的页面,这两个方法也同样无效。
10.1.3 Element 类型
Element
类型用于表现 XML 或 HTML 元素。提供了对元素标签名、子节点及特性的访问。Element
节点具有以下特征:
-
nodeType
的值为 1; -
nodeName
的值为元素的标签名; -
nodeValue
的值为null
; -
parentNode
可能是Document
或Element
; - 其子节点可能是
Element
、Text
、Comment
、ProcessingInstruction
、CDATASection
或EntityReference
。
要访问元素的标签名,可以使用nodeName
属性,也可以使用tagName
属性。
var div = document.getElementById('myDiv');
alert(div.tagName); //"DIV"
alert(div.tagName == div.nodeName); //true
在 HTML 中,标签名始终都以全部大写表示;而在 XML中,标签名则始终会与源代码中的保持一致。最好是在比较之前将标签名转换为相同的大小写形式。
if (element.tagName.toLowerCase() == "div") {}
- HTML 元素
所有 HTML 元素都由HTMLElement
类型表示,不是直接通过这个类型,也是通过它的子类型来表示。HTMLElement
类型直接继承自Element
并添加了一些属性。添加的这些属性分别对应于每个 HTML 元素中都存在的下列标准特性。
-
id
,元素在文档中的唯一标识符。 -
title
,有关元素的附加说明信息,一般通过工具提示条显示出来。 -
lang
,元素内容的语言代码,很少使用。 -
dir
,语言的方向,很少使用。 -
className
,与元素的class
特性对应,即为元素指定的 CSS 类。class
是 ECMAScript 的保留字。
上述属性都可以用来取得或修改相应的特性值。
var div = document.getElementById('myDiv');
alert(div.id);
alert(div.className);
alert(div.title);
alert(div.lang);
alert(div.dir);
div.id = 'someOtherId';
div.className = 'ft';
div.title = 'Some Other text';
div.lang = 'fr';
div.dir = 'rtl';
- 取得特性
每个元素都有一或多个特性,这些特性的用途是给出相应元素或其内容的附加信息。操作特性的 DOM 方法主要有三个,分别是getAttribute()
、setArribute
和removeAttribute()
。
var div = document.getElementById('myDiv');
alert(div.getAttribute('id'));
alert(div.getAttribute('class'));
alert(div.getAttribute('title'));
alert(div.getAttribute('lang'));
alert(dib.getAttribute('dir'));
注意,传递给getAttribute()
的特性名与实际的特性名相同。因此要想得到class
特性值,应该传入class
而不是className
,后者只有在通过对象属性访问特性时采用。
通过getAttribute()
方法也可以取得自定义特性(即标准 HTML 语言中没有的特性)的值。不过,特性的名称是不区分大小写的。另外,根据 HTML5 规范,自定义特性应该加上data-
前缀以便验证。
只有公认的(非自定义)特性才会以属性的形式添加到 DOM 对象中。
有两类特殊的特性,它们虽然有对应的属性名,但属性的值与通过getAttribute()
返回的值并不相同。第一类特性就是style
,用于通过 CSS 为元素指定样式。使用getAttribute()
返回的是 CSS 文本,而通过属性访问则会返回一个对象。
第二类是onclick
这样的事件处理程序。通过getAttribute()
访问时返回的是相应代码的字符串。而在访问onclick
属性时,则会返回一个 JavaScript 函数。
由于存在这些差别。经常不使用getAttribute()
,而是只使用对象的属性。只有在取得自定义特性值的情况下,才会使用getAttribute()
方法。
- 设置特性
setAttribute()
该方法接受两个参数:要设置的特性名和值。如果特性已经存在,setAttribute()
会以指定的值替换现有的值;如果特性不存在,setAttribute()
则创建该属性并设置相应的值。
div.setAttribute('id', 'someOtherId');
div.setAttribute('title', 'Some other text');
div.setAttribute('lang','fr');
直接给属性赋值也可以设置特性的值。
div.id = "someOtherId";
不过,像下面这样为 DOM 元素添加一个自定义的属性,该属性不会自动成为元素的特性。
div.mycolor = "red";
alert(div.getAttribute('mycolor')); //null
要介绍的最后一个方法是removeAttribute()
,这个方法用于彻底删除元素的特性。调用这个方法不仅会清除特性的值,而且也会从元素中完全删除特性。
- attributes 属性
Element
类型是使用attributes
属性的唯一一个 DOM 节点类型。一般不使用。可以用来遍历节点的特性。 - 创建元素
使用document.createElement()
方法可以创建新元素。这个方法只接受一个参数,即标签名。
var div = document.createElement('div');
div.id = "myNewDiv";
div.className = "box";
document.body.appenChild(div);
- 元素的子节点
如果需要通过childNodes
属性遍历子节点,需要检查一下nodeType
属性。
for (var i=0, len = element.childNodes.length; i++) {
if (element.childNodes[i].nodeType == 1) {
//执行某些操作
}
}
10.1.4 Text 类型
文本节点由Text
类型表示,包含的是可以照字面解释的纯文本内容。具有以下特征:
-
nodeType
的值为3; -
nodeName
的值为“ #text ”; -
nodeValue
的值为节点所包含的文本; -
parentNode
是一个Element
; - 不支持(没有)子节点。
使用下列方法可以操作节点中的文本。
-
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
属性,保存着节点中字符的数目。
- 创建文本节点
可以使用document.createTextNode()
创建文本节点,这个方法接受一个参数--要插入节点中的文本。
var textNode = document.createTextNode("<strong>hello</strong>world!");
var element = document.createElement('div');
element.className = "message";
element.appendChild(textNode);
document.body.appendChild(element);
- 规范化文本节点
DOM 文档中存在相邻的同胞文本节点很容易导致混乱,因为分不清哪个文本节点表示哪个字符串。在一个包含两个或多个文本节点的父元素上调用normalize()
方法,则会将所有文本节点合并成一个节点,结果节点的nodeValue
等于将合并前每个文本节点的nodeValue
值拼接起来的值。 - 分割文本节点
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
10.1.5 Comment 类型
注释在 DOM 中是通过Comment
类型来表示的。Comment
节点具有下列特征:
-
nodeType
的值为 8; -
nodeName
的值为“ #comment”; -
nodeValue
的值是注释的内容; -
parentNode
可能是Document
或Element
; - 不支持(没有)子节点。
使用document.createComment()
并为其传递注释文本也可以创建注释节点。
显然,开发人员很少会创建和访问注释节点。
10.1.6 CDATASection 类型
CDATASection
类型只针对基于 XML 的文档,表示的是 CDATA 区域。与Comment
类似,CDATASection
类型继承自Text
类型,因此拥有除splitText()
之外的所有字符串操作方法。
在真正的 XML 文档中,可以使用document.createCDataSection()
来创建 CDATA 区域,只需为其传入节点的内容即可。
10.1.7 DocumentType 类型
该类型并不常用。
10.1.8 DocumentFragment 类型
在所有节点类型中,只有DocumentFragment
在文档中没有对应的标记。可以将它作为一个“仓库”使用。
如果逐个地添加表项,将会导致浏览器反复渲染(呈现)新信息。可以像下面这样使用一个文档片段来保存创建的列表项,然后再一次性将它们添加到文档中。
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);
10.1.9 Attr 类型
元素的特性在 DOM 中以Attr
类型来表示。从技术角度讲,特性就是存在于元素的attributes
属性中的节点。
尽管它们也是节点,但特性却不认为是 DOM 文档树的一部分。最常使用的是getAttribute()
、setAttribute()
和removeAttribute()
方法,很少直接引用特性节点。
10.2 DOM 操作技术
10.2.1 动态脚本
创建动态脚本也有两种方式:插入外部文件和直接插入 JavaScript 代码。
var script = document.createElement('script');
script.type = "text/javascript";
script.src = "client.js";
document.body.appendChild(script);
整个过程可以使用下面的函数来封装:
function loadScript(url) {
var script = document.createElement("script");
script.type = 'text/javascript';
script.src = url;
document.body.appendChild(script);
}
function loadScriptString(code) {
var script = document.createElement('script');
script.type = 'text/javascript';
try {
script.appendChild(document.createTextNode(code));
} catch (ex) {
script.text = code;
}
document.body.appendChild(script);
}
loadScriptString('function sayHi(){alert('hi');}');
10.2.2 动态样式
动态样式是在页面加载完成后动态添加到页面中的。
function loadStyles(url) {
var link = document.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = url;
var head = document.getElementsByTagName('head')[0];
head.appendChild(link);
}
function loadStylesString(css) {
var style = document.createElement('style');
style.type = 'text/css';
try {
style.appendChild(document.createTextNode(css));
} catch (ex) {
style.styleSheet.cssText = css;
}
var head = document.getElementsByTagName('head')[0];
head.appendChild(style);
}
loadStyleString('body{background-color:red}');
10.2.3 操作表格
为了方便构建表格,HTML DOM 还为<table>
、<tbody>
和<tr>
元素添加了一些属性和方法。
10.2.4 使用 NodeList
理解NodeList
及NamedNodeMap
和HTMLCollection
,是从整体上透彻理解 DOM 的关键所在。这三个集合都是“动态的”,每当文档结构发生变化时,它们都会得到更新。
10.3 小结
DOM 是语言中立的 API,用于访问和操作 HTML 和 XML 文档。 DOM 1 级将 HTML 和 XML 文档形象地看作一个层次化的节点树,可以使用 JavaScript 来操作这个节点树,进而改变底层文档的外观和结构。
DOM 由各种节点构成,简要总结如下。
- 最基本的节点类型是
Node
,用于抽象地表示文档中一个独立的部分;所有其他类型都继承自Node
。 -
Document
类型表示整个文档,是一组分层节点的根节点。在 JavaScript 中,document
对象是Document
的一个实例。使用document
对象,有很多种方式可以查询和取得节点。 -
Element
节点表示文档中的所有 HTML 或 XML 元素,可以用来操作这些元素的内容和特性。 - 另外还有一些节点类型,分别表示文本内容、注释、文档类型、CDATA 区域和文档片段。
DOM 操作往往是 JavaScript 程序中开销最大的部分,而因访问NodeList
导致的问题为最多。NodeList
对象都是“动态的”,这就意味着每次访问NodeList
对象,都会运行一次查询。所以,最好的方法就是尽量减少 DOM 操作。