第10章 DOM
DOM(文档对象模型)是针对HTML和XML文档的一个API。DOM描述了一个层次化的节点树,允许开发人员添加、移除和修改页面的某一部分。
注意:IE中所有的DOM对象都是以COM对象的形式实现的。这意味着IE中的DOM对象和原生JavaScript对象的行为或活动特点并不一致。
10.1 节点层次
文档节点是每个文档的根节点,文档节点通常只有一个子节点——文档元素(<HTML>
元素)。
文档元素是稳当的最外层元素,文档中的其他元素都包含在文档元素中。每个文档只能有一个文档元素。在HTML页面中,文档元素始终是<HTML>
元素,在XML中,没有预定义的元素,因此任何一个元素都可以成为文档元素。
10.1.1 Node类型
DOM1级定义了一个Node接口,该接口将由DOM中所有节点类型实现。这个Node接口在JavaScript中是作为Node类型实现的;除了IE之外,在其他所有浏览器中都可以访问到这个类型。JavaScript中所有节点类型都继承自Node类型,因此所有节点类型都共享着基本属性和方法。每个节点都有一个nodeType
属性,用于表明节点的类型。节点类型由在Node类型中定义的下列12个数值常量来表示,任何节点类型必居其一:
1 | ELEMENT_NODE |
2 | ATTRIBUTE_NODE |
3 | TEXT_NODE |
4 | CDATA_SECTION_NODE |
5 | ENTITY_REFERENCE_NODE |
6 | ENTITY_NODE |
7 | PROCESSING_INSTRUCTION_NODE |
8 | COMMENT_NODE |
9 | DOCUMENT_NODE |
10 | DOCUMENT_TYPE_NODE |
11 | DOCUMENT_FRAGMENT_NODE |
12 | NOTATION_NODE |
确定节点类型时,可以将someNode.nodeType
与Node.XXX_NODE
常量相比较,然而,由于IE并没有公开Node类型的构造函数,因此在IE中不能这样确定节点类型。为了确保跨浏览器兼容,最好还是将someNode.nodeType
属性与数字值相比较,如下所示:
if (someNode.nodeType == 1) {
//适用于所有浏览器 1~12
}
并不是所有节点都受到Web浏览器的支持,开发者最常用的是元素节点和文本节点。
nodeName
和nodeValue
属性
要了解节点的具体信息,可以使用节点的nodeName
和nodeValue
属性。在使用这两个节点之前,最好先检测一下节点类型:
if (someNode.nodeType == 1) {
value = someNode.nodeName; //nodeName的值是元素的标签名
}
对于元素节点,nodeName
中保存的始终是元素的标签名,nodeValue
始终为null。
节点关系
每个节点都有一个childNodes
属性,其中保存着一个NodeList对象。NodeList是一种类数组对象,和arguments
一样,并非Array的实例。NodeList的独特之处在于,它实际上是基于DOM结构动态执行查询的结果。因此DOM结构的变化能够自动反映到NodeList对象中。我们常说,NodeList对象是有呼吸,有生命的对象,而不是我们在第一次访问它时拍下的一张快照。访问NodeList对象里的节点有两种方法,方括号(类似于数组)的方式和item()方法。示例:
var firstChild = someNode.childNodes[0];
var secondChild = someNode.childNodes.item(1);
var cpunt = someNode.childNodes.length;//length属性表示childNodes对象中子节点的数量
在前面介绍过,使用Array.prototype.slice()
方法可以将arguments
对象转换为一个数组,同样,使用这个方法可以把childNodes
对象转换为数组:
//在IE8之前的版本中无效
var arrayOfNodes = Array.prototype.slice.call(someNode.childNodes, 0);
因为在IE8之前,childNodes
在IE中并非一个JScript对象,而是一个COM对象,使用上面的语法会抛出错误。在IE浏览器(IE8之前),需要手动枚举childNodes
中的每个子节点,最终组成一个数组:
function convertToArray(nodes) {
var array = null;
try {
array = Array.prototype.slice.call(nodes.childNodes, 0); //针对非IE浏览器
} catch (ex) {
array = new Array();
for (var i = 0;i < nodes.childNodes.length;i++) {
array.push(nodes[i]);
}
i = null;
}
return array;
}
每个节点都有一个parent
属性,里面是它的父节点。包含在childNodes
对象中的所有子节点都有一个共同的父节点。因此他们的parent
属性都指向同一个父节点。此外,包含在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.")
}
如果childNodes
中只有一个节点,那它的previousSibling
属性和nextSibling
属性都为null。
父节点和第一个子节点、最后一个子节点也有特殊的关系。父节点的firstChild
和lastChild
分别指向childNotes
的第一个节点和最后一个节点。如果没有子节点,那么firstChild
和lastChild
都为null。仔细查看原书中图10-2。
hasChildNodes()
方法在父节点包含一个或者多个子节点时返回true
,没有子节点时返回false
。
最后是所有的节点都有的一个属性叫做ownerDocument
,该属性指向表示整个文档的文档节点。这种关系表示的是任何节点都属于它所在的文档。任何节点都不能同时存在于两个或更多文档中,通过这个属性,我们不必层层回溯到达顶端,而是可以直接访问文档节点。
虽然所有节点都继承自Node节点,但并非所有节点都有子节点。
操作节点
因为关系指针都是只读的,所以DOM提供了一系列操作节点的方法。
appendChild()
:添加一个新节点到父节点childNotes
的最后一个位置并返回这个节点
var returnedNode = someNode.appendChild(newNode);
alert(returnedNode == newNode); //true
alert(someNode.lastChild == newNode); //true
如果添加的新节点是文档中已有节点的话,则改变它的位置(将这个节点移动到最后)
//someNode有多个子节点
var returnedNode = someNode.appendChild(someNode.firstChild);
alert(returnedNode == someNode.firstChild); //false 第一个子节点已经改变
alert(returnedNode == someNode.lastChild); //true
insertrefore()
:插入节点,将要插入的节点放在父节点childNotes
的某个位置上,接收两个参数。参数一是要插入的节点,参数二是作为参照的节点。结果是要插入的节点将插入到参照节点之前(成为同胞节点,参数一是参数二的previousSibling
)。方法返回要插入的节点(即参数一)。
参数二为null时,和appendChild()
方法一样,插入到childNotes的最后。
//插入后成为最后一个子节点
returnedNode = someNode.insertrefore(newNode, null);
alert(returnedNode == someNode.lastChild); //true
//插入后成为第一个子节点
returnedNode = someNode.insertrefore(newNode, someNode.firstChild);
alert(returnedNode == newNode); //true
alert(newNode == someNode.firstChild); //true
//插入到最后一个子节点前面
returnedNode = someNode.insertrefore(newNode, someNode.lastChild);
alert(newNode == someNode.childNodes[someNode.childNodes.length - 2]); //true
replaceChild()
:替换节点。接收两个参数,参数一是要插入到childNotes
中的节点(新节点),参数二是被替换的节点(旧节点)。被替换的节点将从文档树中被移除,由参数一代表的要插入的节点占据其位置(参数一替换参数二)。方法返回被替换的节点(参数二)。在使用replaceChild()
替换一个节点时,被替换的节点所有关系指针将被复制到要插入的节点上。从技术角度讲,被替换的节点还在文档中,但是在文档树中已经没有了它的位置。
//替换第一个子节点
var returnedNode = someNode.replaceChild(newNode, someNode.firstChild);
//替换最后一个子节点
returnedNode = someNode.replaceChild(newNode, someNode.lastChild);
removeChild()
:移除指定节点,返回被移除的节点。被remove的节点还在文档中,但是在文档树中已经没有了它的位置
//移除第一个子节点
var formerFirstChild = someNode.removeChild(someNode.firstChild);
//移除最后一个子节点
var formerLastChild = someNode.removeChild(someNode.lastChild);
上面介绍的四个方法操作的都是某个节点的子节点,也就是说,要使用这几个方法要先取得父节点,另外,不是所有的节点都有子节点,如果在不支持子节点的节点上调用了这些方法,将会导致错误发生。
其他方法
有两个方法是所有类型的节点都有的:cloneNode()
normalize()
。
cloneNode()
:创建一个调用这个方法的节点的一个副本。接收一个参数,参数值为true和false。true表示深复制,将复制整个节点和子节点树;false表示浅复制,只复制这个节点,不复制其子节点树。复制后返回的节点归文档所有,但没有为它指定父节点,因此这个节点副本就成了一个“孤儿”。除非通过appendChild()、insertrefore()或者replaceChild()方法将它添加到文档中。
假设有如下HTML代码:
<ul id="ul"><li>item1</li><li>item2</li><li>item3</li></ul>
脚本:
var mylist = document.getElementryId("ul");
var deepList = mylist.cloneNode(true);
alert(deepList.childNodes.length); //3
var shallowList = mylist.cloneNode(false);
alert(shallowList.childNodes.length); //0
如果HTML代码如下:
<ul id="ul">
<li>item1</li>
<li>item2</li>
<li>item3</li>
</ul>
那么脚本代码结果将变为:
var mylist = document.getElementryId("ul");
var deepList = mylist.cloneNode(true);
alert(deepList.childNodes.length); //7
var shallowList = mylist.cloneNode(false);
alert(shallowList.childNodes.length); //0
之所以(deepList.childNodes.length
的值会发生改变,是因为IE(IE<9)和其他浏览器处理空白文本节点的机制不一样 ,FF,谷歌,IE>=9浏览器会将各个节点之间的空白也算作一个节点,但是IE(IE<9)不会。
解决方法一:像上面那样修改HTML源码节点之间无空格。
解决方法二:调用childNodes属性之前先将空格删除 (HTML源码可以缩进),脚本代码为:
var mylist = document.getElementryId("ul");
var deepList = mylist.cloneNode(true);
for (var i = 0; i < deepList.childNodes.length; i++) {
//如果是文本节点,并且值为空,则删除该节点
if (deepList.childNodes[i].nodeType == 3 && /\s/.test(deepList.childNodes[i].nodeValue)) {
deepList.childNodes[i].parentNode.removeChild(deepList.childNodes[i]);
}
}
alert(deepList.childNodes.length); //3
var shallowList = mylist.cloneNode(false);
alert(shallowList.childNodes.length); //0
nodeType
为常数3时表示节点为文本节点,第二个条件为使用正则表达式判断当前这个节点中是否含有空格(\s)。
normalize()
:这个方法的唯一作用就是处理文档树中的文本节点。由于解析器的实现或者DOM操作等原因,可能会出现文本节点不包含文本,或者接连出现两个文本节点的情况。当在某个节点上调用这个方法时,就会在该节点的后代节点中查找上述两种情况。如果找到了空文本节点则删除它,如果找到了两个相邻的文本节点则将它们合并为一个文本节点。
10.1.2 Document类型
JavaScript中通过Document类型表示文档。在浏览器中,document
对象是HTMLDocument(继承自Document类型)的第一个实例,表示整个HTML页面。而且,document
对象是window
对象的属性,因此可以将其作为全局对象来访问。Document节点具有以下特征:
nodeType
的值为9nodeName
的值为“#document”nodeValue
的值为nullparentNode
的值为nullownerDocument
的值为null- 其子节点可能是一个DocumentType(最多一个)、Element(最多一个)、ProcessingInstruction或Comment。
Document类型可以表示HTML页面或者其他基于XML的文档。不过,最常见的应该是作为HTMLDocument实例的document对象。通过这个对象,不仅能够取得与页面有关的信息,还可以操作页面的外观和底层结构。
补充:在Firefox、Safari、Chrome和Opera中,可以通过脚本访问Document类型的构造函数和原型。但在所有浏览器中都可以访问HTMLDocument类型的构造函数和原型。
文档的子节点
虽然DOM标准规定Document节点的子节点可以是DocumentType、Element、ProcessingInstruction或Comment,但还有两个内置的访问其子节点的快捷方式,第一个就是documentElement
属性,该属性始终指向HTML页面中的<html>
元素。另一个就是通过childNotes
列表访问文档元素,但通过documentElement
属性能更快捷、更直接的访问该元素。示例;
HTML代码:
<html lang="en">
<body>
</body>
</html>
脚本代码:
var html = document.documentElement;
alert(html === document.firstChild); //true
alert(html === document.childNodes[0]); //true
documentElement
虽然永远指向<html>
元素,但是document.firstChild
和document.childNodes[0]
不一定。
document
还有一个body
属性,指向<body>
元素,开发人员经常要使用这个元素,因此document.body
在JavaScript代码中出现的频率非常高。
var body = document.body;
所有浏览器都支持document.documentElement
和document.body
属性。
Document另一个可能的子节点是DocumentType。通常将<!DOCTYPE>
标签看成一个与文档其他部分不同的实体,可以通过doctype
属性(浏览器中是document.type
来访问它的信息):
vardoctype=document.doctype;
浏览器对document.doctype
的支持差别很大,可以给出如下总结:
1.IE8及之前版本:如果存在文档类型声明,会将其错误的解释为一个注释并把它当做Comment节点;而`document.doctype`的值始终为null。
2.IE9+及Firefox:如果存在文档类型声明,则将其作为文档的第一个子节点;`document.doctype`是一个DocumentType节点,也可以通过`document.firstChild`或者`document.childNodes[0]`访问同一个节点。
3.Safari、Chrome和Opera:如果存在文档类型声明,则将其解析,但不作为文档的子节点。`document.doctype`是一个DocumentType节点,但该节点不会出现在document.childNodes中。
由于浏览器对这个属性的支持不一致,因此这个属性的用处也很有限。
文档信息
作为HTMLDocument的一个实例,document
对象还有一些标准的Document对象所没有的属性。这些属性提供了document
对象所表现的一些网页的信息。其中第一个属性就是title
,包含着<title>
元素的文本——显示在浏览器的标题栏或者标签页上。修改title的值就是修改<title>
元素中文本的值,不会影响<title>
元素。示例:
vartitleName=document.title;
alert(titleName);
document.title="修改后的title";//会反映在标签页或者标题栏上
接下来的3个属性都与浏览器请求有关,分别是URl
、domain
和referrer
。URL
属性包含页面完整的URL,domain
属性只包含页面的域名,而referrer
属性则保存着链接到当前页面的那个页面的URL。在没有来源页面的情况下,referrer
属性中可能存放的是空字符串。所有的这些信息都存储在HTTP请求头部,只不过通过这些属性让我们可以访问到它们而已,如下所示:
//获得完整的URL
var url = document.URL;
//取得域名
var domain = document.domain;
//取得来源页面的URL
var referrer = document.referrer;
URL
和domain
属性是相关联的。例如,如果document.URL
等于https://www.baicu.com/s,那么document.domain
就等于 www.baidu.com 。在这三个属性中,只有domain
是可以设置的,但出于安全方面的限制,也并非可以给domain
设置任何值。书中貌似可以给domain
属性设置值,但是我查了W3SCHOOL,上面指出该属性是只读的:
该属性是一个只读的字符串,包含了载入当前文档的 web 服务器的主机名。
来自 http://www.w3school.com.cn/jsref/prop_doc_domain.asp
为了验证,我写了一个试试:
<html>
<body>
本文档的域名是:
<script type="text/javascript">
document.write(document.domain);
document.domain = "w3school.com.cn";
document.write("<br>本文档的域名是:"+document.domain);
</script>
</body>
</html>
运行结果:
本文档的域名是: www.w3school.com.cn
本文档的域名是:w3school.com.cn
这段代码并非在编辑器中所写,而是在W3SCHOOL中的示例代码中修改的。
书中说的是正确的,domain
属性确实可以修改,但是domain
不能设置为URL中不包含的域。
如下所示:
<html>
<body>
本文档的域名是:
<script type="text/javascript">
document.write(document.domain);
document.domain = "baidu.com.cn";
document.write("<br>本文档的域名是:"+document.domain);
</script>
</body>
</html>
我修改了domain
设置的值,运行后结果:
本文档的域名是: www.w3school.com.cn
这说明domain
设置是有限制的(domain
设置的值必须在当前这个网站下,不可以设置为别的网站的域名)。
通过设置domain
属性,可以实现来自不同子域的页面之间的JavaScript通信。当页面中含有来自其他子域的框架或者内嵌框架时,由于跨域安全限制,来自不同子域的页面是无法通过JavaScript进行通信的,通过设置这些子域页面的domain
属性为相同的值,这些页面就可以互相访问对方包含的JavaScript对象了。举例:
有一个页面加载自www.baidu.com,其中包含一个内嵌框架,框架内的页面加载自p2p.baidu.com,由于
document.domain
中保存的字符串不同,内外两个页面之间无法访问对方的JavaScript对象,但如果将两个页面的domain
属性都设置为baidu.com,则它们之间就可以相互通信了。
浏览器对domain
属性还有一个限制,即如果域名一开始是“松散的”(loose),那么不能将它再设置为“紧绷的”(tight)。换句话说,将document.domain
属性设置为“baidu.com”之后,无法再将domain
属性设置为“www.baidu.com”,否则将会导致错误。如下所示:
//假设页面来自www.baidu.com
document.domain = "baidu.com"; //松散的(成功)
document.domain = "www.baidu.com"; //紧绷的(出错!)
所有的浏览器都存在这个限制,但IE8是最早实现这个限制的版本。
查找元素
查找元素的三个方法:
document.getElementryId();
document.getElementsByTagName();
document.getElementsByName();
document.getElementryId()
:接收一个参数,参数值为文档中元素节点的id特性值(Attribute),返回这个元素。参数id严格匹配,区分大小写(IE8及较低版本不区分大小写)。如果页面中多个元素的id值相同,则只返回文档中第一次出现的元素。IE7及较低版本还有一个怪癖:name特性与给定ID匹配的表单元素也会被该方法返回。
document.getElementsByTagName()
:接收一个参数,即元素的标签名,返回包含零或多个元素的NodeList。在HTML文档中,这个方法会返回一个HTMLCollection对象,作为一个“动态”集合,该对象与NodeList非常类似,可以使用方括号语法或者item()
方法来访问HTMLCollection中的项,同样具有length
属性。方括号中可以是数值索引和字符串索引(字符串必须为HTMLCollection中某个元素的name特性值)。HTMLCollection还有一个方法,叫做namedItem()
,使用这个方法可以通过元素的name
属性取得集合中的项。示例:
//获取所有<img>标签元素
var images = document.getElementsByTagName("img");
//输出图片的数量
alert(images.length);
//输出第一个图像的src特性值
alert(images[0].src);
//输出第一个图像的src特性值
alert(images.item(0).src);
假设现在有这么一个元素:
<img src="" alt="" name="myImage">
在上面的“images”变量中取得这个元素:
var myImage = images.namedItem("myImage");
或者
var myImage = images["myImage"];
\对于HTMLCollection而言,我们可以向方括号中传入数值或者字符串形式的索引值,在后台,对数值索引就会调用item(),对字符串索引就会调用namedItem()。
向document.getElementsByTagName()
中传入一个“”表示通配符全部,即可以获取整个页面中所有的元素(按照先后顺序)。补充:IE将注释(Comment)实现为元素(Element),因此IE中传入“”会返回所有注释节点。传给document.getElementsByTagName()
中的参数是不区分大小写的。
document.getElementsByName()
:接收一个参数,参数值为页面中某个元素的name
特性值。最常使用在单选按钮中,操纵一组单选按钮,因此一组单选按钮的name
特性值一般相同。该方法也返回一个HTMLCollection,但是对于一组name
相同的元素来说,使用字符串索引或者调用namedItem()
方法则只会取得集合中的第一项(因为每一项的name特性值都相同)。
特殊集合
除了属性和方法,document
对象还有一些特殊的集合。这些集合都是HTMLCollection对象,为访问文档提供了快捷方式。
- document.anchors:返回文档中所有带有name特性的元素。
- document.applets:包含文档中所有的元素,因为不再推荐使用元素,所以不建议使用这个集合。
- document.forms:包含文档中所有的元素,与document.getElementryTagName(“form”)得到的结果相同。
- document.images:包含文档中所有的元素,与document.getElementryTagName(“img”)得到的结果相同。
- document.links:包含文档中所有带href特性的元素。
上面这些集合始终都可以通过HTMLDocument对象访问到,而且,和HTMLCollection对象类似,集合中的项也会随着当前文档内容的更新而更新。
DOM一致性检测
由于DOM分为多个级别,也包含多个部分,因此检测浏览器实现了哪些DOM标准就十分必要了。document.implementation
属性就是为此提供相应信息和功能的对象,它提供了一个hasFeature()
方法,该方法接收两个参数,参数一是DOM功能的名称,参数二是版本号。如果浏览器支持给定名称和版本的功能则返回true
,不支持则返回false
。示例:
var hasXMLDom = document.implementation.hasFeature("XML", "1.0");
alert(hasXMLDom); //true
document.implementation
属性貌似已经被弃用了……
https://developer.mozilla.org/en-US/docs/Web/API/DOMImplementation/hasFeature
文档写入
将输出流写入到网页。write()
、writeln()
、open()
、close()
。
document.write()
和document.writeln()
方法都接收一个字符串参数,write()
会原样输出,而writeln()
会在字符串的末尾添加一个换行符(\n)。在页面加载的过程中,可以向页面动态地加载内容:
<p>The current date and time is:
<script type="text/javascript">
document.write("<b>" + new Date().toString() + "<\/b>>");
</script>
</p>
除此之外,还可以使用document.write()
和document.writeln()
方法动态地包含外部资源(如JS文件),在包含JS脚本时,要注意不能直接包含字符串</script>
,而是要将斜杠进行转义。否则将会导致字符串中的</script>
解释为脚本代码结束,之后的代码将无法运行。示例;
<p>The current date and time is:
<script type="text/javascript">
document.write("<script type\"text\/javascript\" src=\"example.js\">" + "<\/<script>");
</script>
</p>
前面的例子都是在文档加载时向网页输出内容,如果在页面加载完成后再调用document.write()
或document.writeln()
,输出的内容将会重写整个页面:
window.onload = function () {
document.write("重写页面!");
};
使用window.onload
事件,表示页面加载后再执行函数。
open()
和close()
分别用于打开和关闭网页的输出流。如果是在页面加载期间使用write()
和writeln()
方法,则不需要使用这两个方法。
严格类型的XHTML文档不支持文档写入。对于那些按照application/xml+xhtml内容类型提供的页面,write()和writeln()也同样无效。
10.1.3 Element类型
除了Document类型以外,Element类型是最常使用的类型了。Element类型用于表现XML或HTML元素,提供了对元素标签名,子节点及特性的访问。Element类型具有以下特征:
nodeType
的值为1nodeName
的值为元素的标签名nodeValue
的值为nullparentNode
可能是Document也可能是Element- 其子节点可能是Element、Text、Comment、ProcessingInstruction、CDTASection或者EntityReference
要访问元素的标签名,可以使用nodeName
属性,也可以使用tagName
属性。后者更为清晰:
HTML:<divid="mydiv"></div>
脚本代码:
varmydiv=document.getElementryId("mydiv");
alert(mydiv.tagName);//"DIV"
alert(mydiv.tagName===mydiv.nodeName);//true
在HTML中,标签名全部以大写表示;而在XML(有时候也包括XHTML)中,标签名则始终会与源代码中保持一致。假如不确定脚本将会在HTML还是XML文件中运行,最好是在比较之前将标签名转换为相同的大小写形式:
//不要这样做,很容易出错!
if(element.tagName=="div"){
//执行某些代码……
}
//下面的写法适用于任何文档
if(element.tagName.toLowerCase()=="div"){
//执行某些代码……
}
在比较之前转换为小写(大写)形式再比较。
HTML元素
所有的HTML元素都由HTMLElement类型表示,不是直接通过这个类型,也是通过它的子类型来表示。HTMLElement直接继承自Element并添加了一些属性,添加的这些属性分别对应html元素中的下列标准特性:
id
,元素在文档中的唯一标识符。title
,有关元素的附加说明信息,一般通过工具提示条显示出来。lang
,元素内容的语言代码,现在很少使用了。dir
,语言的方向,值为“ltr”(left to right)和“rtl”(right to left),也很少使用。className
,与元素的class属性对应,即为元素指定的CSS类。没有将这个属性命名为“class”,是因为class在ECMAScript中是保留字。
上面这些属性都可以用来取得或者修改相应的特性值。
取得特性
每个元素都有一个或多个特性,这些特性的用途是给出相应元素或其内容的附加信息。操作特性的DOM方法主要有三个,分别是getAttribute()
、setAttribute()
和removeAttribute()
。这三个方法可以对任何特性使用。示例:
<body>
<div id="myDiv" class="bd" title="Body text" lang="en" dir="ltr"></div>
</body>
<script type="text/javascript">
var myDiv = document.getElementryId("myDiv");
console.log(myDiv.getAttribute("id")); //myDiv
console.log(myDiv.getAttribute("class")); //bd
console.log(myDiv.getAttribute("title")); //Body text
console.log(myDiv.getAttribute("lang")); //en
console.log(myDiv.getAttribute("dir")); //ltr
</script>
注意:传递给getAttribute()
的特征名和实际的特征名相同。因此要得到class
特性的值,就应该传入“class”而不是“className”,后者只有通过对象属性访问时才用。如果给定名称的特性不存在,方法返回null。
通过getAttribute()
方法也可以获取自定义属性,示例:
<body>
<div id="myDiv" class="bd" title="Body text" lang="en" dir="ltr" my_special_attribute="hello!"></div>
</body>
<script type="text/javascript">
var myDiv = document.getElementryId("myDiv");
var value = myDiv.getAttribute("my_special_attribute"); //hello!
</script>
特性的名称是布区分大小写的,即“ID”和“id”是同一个特性。
根据HTML5规范,自定义特性应该加上data-
前缀以便验证。
任何元素的所有属性,也都可以通过DOM元素本身的属性来访问。当然,HTMLElement也会有5个属性与相应的特性一一对应。不过,只有公认的(非自定义的)特性才会以属性的形式添加到DOM对象中。示例:
<body>
<div id="myDiv" class="bd" title="Body text" lang="en" dir="ltr" my_special_attribute="hello!"></div>
</body>
<script type="text/javascript">
var myDiv = document.getElementryId("myDiv");
console.log(myDiv.id); //myDiv
console.log(myDiv.className); //bd
console.log(myDiv.title); //Body text
console.log(myDiv.lang); //en
console.log(myDiv.dir); //ltr
console.log(myDiv.my_special_attribute); //undefined(IE除外)
</script>
自定义特性在Safari、Chrome、Opera和Firefox中是不存在的,但是IE却会为自定义属性也创建特性。
有两类特殊的特性,它们虽然有对应的属性名,但属性值与通过getAttribute()
返回的值并不相同。第一类特性就是style
,用于通过CSS元素指定样式。在通过getAttribute()
访问时,返回的style
特性值中包含的是CSS文本,而通过属性访问style
则返回一个对象。由于style
特性是用于以编程方式访问元素样式,因此并没有直接映射到style
特性。第二类与众不同的特性是onclick
这样的事件处理程序。当在元素上使用时,onclick
特性中包含的是JavaScript代码,如果通过getAttribute()
方法访问,返回的是相应代码的字符串。而在访问onclick
属性时,则返回一个JavaScript函数(如果未在元素中指定相应的特性,则返回null)。这是因为onclick
等事件处理程序特性本身就应该赋予函数值。
由于存在上面的差别,因此在通过JavaScript以编程方式操作DOM时,开发人员不经常使用getAttribute()
,而是只使用对象的属性,只有在取得自定义属性的情况下,才会使用getAttribute()
方法。示例:
<body>
<div my_special_attribute="hello!" style="color: red;background-color: aqua" onclick="myDivOnclick()" id="myDiv">
<span>测试文本</span>
</div>
</body>
<script type="text/javascript">
function myDivOnclick() {
alert("You click me!");
}
var myDiv = document.getElementById("myDiv");
console.log(myDiv.getAttribute("style")); //返回CSS代码
console.log(myDiv.style); //返回一个对象
console.log(myDiv.getAttribute("onclick")); //返回相应代码的字符串
console.log(myDiv.onclick); //返回一个JavaScript函数
console.log(myDiv.getAttribute("my_special_attribute")); //hello!
console.log(myDiv.my_special_attribute); //undefined
</script>
*设置特性
与getAttribute()
对应的方法是 setAttribute()
,这个方法接收两个参数:要设置的特性名和特性值。如果特性已经存在,setAttribute()
会以指定的值替换现有的值;如果特性不存在,setAttribute()
则创建该属性并设置相应的值。示例:
<body>
<div my_special_attribute="hello!" style="color: red;background-color: aqua" onclick="myDivOnclick()" id="myDiv">
<span>测试文本</span>
</div>
</body>
<script type="text/javascript">
function myDivOnclick() {
alert("You click me!");
}
var myDiv = document.getElementById("myDiv");
myDiv.setAttribute("id","some other id");
myDiv.setAttribute("class", "ft");
myDiv.setAttribute("title", "some other text");
myDiv.setAttribute("lang","fr");
myDiv.setAttribute("dir","rtl");
console.log(myDiv.getAttribute("id")); //some other id
</script>
通过setAttribute()
方法设置了某元素各项特性后,并不会对已经取得该元素的变量产生任何影响,继续使用已经取得该元素的变量依然可以进行各种操作。除了使用setAttribute()
方法外,可以直接设置某元素的属性,示例:
<div my_special_attribute="hello!" style="color: red;background-color: aqua" onclick="myDivOnclick()" id="myDiv">
<span>测试文本</span>
</div>
</body>
<script type="text/javascript">
function myDivOnclick() {
alert("You click me!");
}
var myDiv = document.getElementById("myDiv");
myDiv.id = "some other id";
myDiv.className = "ft";
myDiv.title = "some other text";
myDiv.lang = "fr";
myDiv.dir = "rtl";
console.log(myDiv.getAttribute("id")); //some other id
</script>
但是,为DOM元素添加一个自定义属性,该属性不会自动成为元素的特性。示例:
<body>
<div my_special_attribute="hello!" style="color: red;background-color: aqua" onclick="myDivOnclick()" id="myDiv">
<span>测试文本</span>
</div>
</body>
<script type="text/javascript">
function myDivOnclick() {
alert("You click me!");
}
var myDiv = document.getElementById("myDiv");
myDiv.myAttr = "world!";
alert(myDiv.getAttribute("myAttr")); //null
</script>
推荐开发者通过属性来设置特性。
最后一个方法removeAttribute
,这个方法用于彻底删除元素特性,调用这个方法不仅可以删除特性值,而且会在元素中完全删除特性。示例:
<body>
<div my_special_attribute="hello!" style="color: red;background-color: aqua" onclick="myDivOnclick()" id="myDiv">
<span>测试文本</span>
</div>
</body>
<script type="text/javascript">
function myDivOnclick() {
alert("You click me!");
}
var myDiv = document.getElementById("myDiv");
myDiv.removeAttribute("my_special_attribute");
alert(myDiv.getAttribute("my_special_attribute")); //null
</script>
attributes
特性
Element类型是使用attributes
属性的唯一一个DOM节点类型,attributes
属性中包含一个NamedNodeMap,与NodeList类似,也是一个“动态”的集合。元素的每一个特性都由一个Attr节点表示,每个节点都保存在NamedNodeMap对象中,NamedNodeMap对象具有以下的方法。
- getNamedItem_(name)_:返回nodeName属性等于name的节点。
- removeNamedItem_(name)_:从列表中移除nodeName属性等于name的节点。
- setNamedItem_(name)_:向列表中添加节点,以节点的nodeName属性为索引。
- item_(pos)_:返回位于数字pos位置处的节点。
attributes
属性中包含一系列的节点,每个节点的nodeName就是特性的名称,而节点的nodeValue就是特性的值。取得元素的id
特性,示例:
<body>
<div style="color: red;background-color: aqua" id="myDiv">
<span>测试文本</span>
</div>
</body>
<script type="text/javascript">
var myDiv = document.getElementById("myDiv");
var id = myDiv.attributes.getNamedItem("id").nodeValue;
alert(id); //myDiv
</script>
问题:不是很理解下面这段代码:
<body>
<div style="color: red;background-color: aqua" id="myDiv">
<span>测试文本</span>
</div>
</body>
<script type="text/javascript">
// var myDiv = document.getElementById("myDiv");
var id = myDiv.attributes.getNamedItem("id").nodeValue;
alert(id); //myDiv
</script>
将获取id
特性值为myDiv的element元素的代码注释掉后,脚本中的代码正常执行并返回了正确的结果,但是脚本代码第一行var id = myDiv.attributes.getNamedItem("id").nodeValue;
中的myDiv并未定义,不知道为什么仍能正确运行……
以下是使用方括号语法通过特性名访问节点的简写方式:
<body>
<div style="color: red;background-color: aqua" id="myDiv">
<span>测试文本</span>
</div>
</body>
<script type="text/javascript">
// var myDiv = document.getElementById("myDiv");
var id = myDiv.attributes["id"].nodeValue;
alert(id); //myDiv
</script>
还是上面那个问题,未定义myDiv却可以使用。
也可以使用方括号语法将某个特性设置为一个新值:
<body>
<div style="color: red;background-color: aqua" id="myDiv">
<span>测试文本</span>
</div>
</body>
<script type="text/javascript">
var element = document.getElementById("myDiv");
element.attributes["id"].nodeValue = "other id";
alert(element.id); //other id
</script>
使用removeNamedItem()
删除某个特性(与在元素中调用removeAttribute()
方法效果相同),两者之间有一点小小的不同就是使用removeNamedItem()
会返回被删除了指定特性的节点。示例:
<body>
<div style="color: red;background-color: aqua" id="myDiv" my_special_attr="hello!">
<span>测试文本</span>
</div>
</body>
<script type="text/javascript">
var element = document.getElementById("myDiv");
var oldAttr = element.attributes.removeNamedItem("id");
alert(oldAttr); //[object Attr]
alert(element.id); //空字符串
var antherOldValue = element.attributes.removeNamedItem("my_special_attr");
alert(antherOldValue); //[object Attr]
alert(element.getAttribute("my_special_attr")); //null
</script>
补充:其实对书中的说法持怀疑态度,在多次实验后,我发现只有自定义特性才会被真的删除(值和特性都被删除),而非自定义特性(例如id,class,style……)只会被删除特性值而不会删除特性!
最后,setNamedItem()
是一个不常用的方法,通过这个方法可以为元素添加一个新特性,为此需要传入一个新的特性节点。示例:
element.attributes.setNamedItem(newAttr);
一般来说,使用attributes
的方法不够方便,因此开发人员更多的会使用getAttribute()
、setAttribute()
和removeAttribute()
方法。不过,如果要遍历元素的特性,attributes
属性就可以派上用场了,在需要将DOM结构序列化为XML和HTML字符串时,多数都会涉及遍历元素特性。示例:
<body>
<div style="color: red;background-color: aqua" id="myDiv" my_special_attr="hello!">
<span>测试文本</span>
</div>
</body>
<script type="text/javascript">
function outputAttributes(element) {
var paris = 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;
paris.push(attrName + "=\"" + attrValue + "\"");
}
return paris.join(" ");
}
alert(outputAttributes(document.getElementById("myDiv"))); //style="color: red;background-color: aqua" id="myDiv" my_special_attr="hello!"
</script>
使用了一个数组来保存名值对,最后再以空格为分隔符将它们拼接起来(这是序列化长字符串的一种常用技巧)。注意两点:一是不同的浏览器在遍历元素的attributes
时,返回特性的顺序不同,二是在IE7及更早的版本中,会返回HTML中所有可能的特性,包括没有指定的特性(换句话说,就是可能返回一百多个特性)。
针对IE7及其以下版本中返回未指定特性的问题,可以对上面的函数加以改进:
for (i = 0, len = element.attributes.length; i < len; i++) {
attrName = element.attributes[i].nodeName;
attrValue = element.attributes[i].nodeValue;
if (element.attributes[i].specified) {
paris.push(attrName + "=\"" + attrValue + "\"");
}
}
只需修改其中的for循环部分。使用了每个特性节点都有的一个属性:specified
,这个属性的值为true时,表示要么在HTML代码中指定了相应的特性,要么是在脚本中使用setAttribute()
方法设置了该特性。在IE中,所有未设置过的特性的该属性值都为false,而在其他浏览器中根本不会为这类特性生成对应的特性节点(因此,在这些浏览器中,任何特性节点的specified
值始终为true)。
创建元素
使用document.createElement()
可以创建新元素,这个方法只接收一个参数,即要创建元素的标签名。这个标签名在HTML文档中不区分大小写,在XML(包括XHTML)文档中区分大小写。示例:
var div = document.createElement("div");
在使用document.createElement()
方法创建新元素的同时,也为新元素设置了ownerDocument
属性。此时,还可以操作元素的特性,为其添加更多的节点。示例:
div.id = "myNewId";
div.className = "box";
在新元素上设置了这些特性只是赋予了它们相应的信息,由于新元素尚未被添加到文档树中,因此设置这些特性也不会影响浏览器的显示。要把新元素添加到文档中,需要使用appendChild()
、insertBefore()
或者replaceChild()
方法。示例:
var div = document.createElement("div");
div.id = "myNewId";
div.className = "box";
document.body.appendChild(div);
一旦将元素添加到文档树中,那么该元素就会在浏览器中立即呈现出来,此后,对该元素的任何修改,都会呈现在浏览器中。
在IE中可以使用另一种方式使用用document.createElement()
方法,即为这个方法传入完整的HTML标签,也可以包含属性。示例:
var div = document.createElement("<div id=\"myNewId\" class=\"box\"></div>");
使用这种方式有助于避开在IE7及更早版本中动态创建元素的某些问题(具体的问题在书中已经详细的列举出来了)。其他浏览器都不支持这种用法(传入完整的HTML标签创建DOM元素)。
document.createElement()
方法会返回一个DOM元素的引用。可以将这个引用添加到DOM文档中,也可以对其加以增强。
元素的子节点
这里的问题就是在前面曾经出现过的问题,在 10.1.1 Node类型一章中的“其他方法”这一部分中有过详细的解释。下面解决如何通过某个特定的标签名获取其子节点或者后代节点。解决方法很简单,因为元素也支持getElementsByTagName()
方法,在通过元素调用方法时,除了搜索起点是当前这个元素之外,其他方面都跟document
调用这个方法是相同的,因此结果只会返回当前元素的后代。示例:
<body>
<ul id="myList">
<li>item1</li>
<li>item2</li>
<li>item3</li>
</ul>
</body>
<script type="text/javascript">
var myList = document.getElementById("myList");
var items = myList.getElementsByTagName("li");
alert(items.length); //3
</script>
值得注意的是:这里的<ul>
元素的后代中只包含了直接子元素。不过,如果它包含更多层次的后代元素,那么各个层次中包含的<li>
元素也会返回。
10.1.4 Text类型
文本节点由Text类型表示,包含的是可以照字面解释的纯文本内容,纯文本中可以包含转义后的HTML字符,但是不能包含HTML代码。Text节点具有以下几个特征:
nodeType
的值为3nodeName
的值为”#text”nodeValue
的值为节点包含的文本parentNode
是一个Element- 不支持(没有)子节点
可以使用nodeValue
属性或者data
属性访问Text节点的内容,两个属性中包含的内容相同,都是Text节点中包含的文本。对其中一个属性的修改会反映到另一个属性上。使用下列方法可以操纵节点中的文本:
appendData(text)
:将text文本添加到节点的末尾deleteData(offset,count)
:从offset开始的位置删除count个字符insertData(offset,count)
:在offset指定的位置插入textreplaceData(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,取决于文档类型)编码,示例:
<body>
<div id="myDiv">Hello world!</div>
</body>
<script type="text/javascript">
var myDiv = document.getElementById("myDiv");
//修改的部分内容的源代码是:
//<div id="myDiv">Some <strong>other</strong> message</div>
myDiv.firstChild.nodeValue = "Some <strong>other</strong> message";
</script>
Firefox中,选中该<div>
元素后,右键“查看选中部分源代码”,即可看到上面注释中的代码。
整个页面的源代码仍然是上面的部分
创建文本节点
使用document.createTextNode()
创建一个新的文本节点,这个方法接收一个参数,就是要要插入文本节点中的文本。与上面修改已有文本节点的文本值一样,作为参数的文本也将按照HTML或XML的格式进行编码。示例:
var textNode = document.createTextNode("Some <strong>other</strong> message");
在创建出新的文本节点时,也会为其自动设置ownerDocument
属性。不过,除非把新节点添加到文档树已经存在的节点中,否则不会在浏览器窗口中看到这个添加的新文本节点。示例:
<body>
<!--
查看选中部分源代码
<div class="message">Hello world!</div>
-->
</body>
<script type="text/javascript">
var element = document.createElement("div");
element.className = "message";
var textNode = document.createTextNode("Hello world!");
element.appendChild(textNode);
document.body.appendChild(element);
</script>
创建了一个<div>
元素并为其添加了一个文本节点,然后将创建的<div>
元素添加到body
中。
一般情况下,每个元素只能有一个文本节点,但是在某些情况下也会有多个文本节点。示例:
<script type="text/javascript">
var element = document.createElement("div");
element.className = "message";
var textNode = document.createTextNode("Hello world!");
element.appendChild(textNode);
//再添加一个文本节点
var antherTextNode = document.createTextNode("Yippee!");
element.appendChild(antherTextNode);
document.body.appendChild(element);
</script>
如果两个文本节点是同胞(兄弟)节点,那么这两个节点中的文本就会连接起来,中间不会有空格。
规范化文本节点
DOM文档中存在相邻的同胞文本节点很容易导致混乱,因为分不清哪个文本节点表示哪个字符串。由于DOM文档中出现同胞文本节点的情况不在少数,因此催生了一个能够将相邻的文本节点合并的方法——normalize()
,这个方法是由Node类型定义的(因而在所有的节点类型中都存在)。在一个包含两个或者两个以上文本节点的父元素上调用normalize()
方法,则会将所有的文本节点合并为一个文本节点,合并后的节点的nodeValue
值为合并前所有文本节点的nodeValue
值的拼接字符串。示例:
<script type="text/javascript">
var element = document.createElement("div");
element.className = "message";
var textNode = document.createTextNode("Hello world!");
element.appendChild(textNode);
//再添加一个文本节点
var antherTextNode = document.createTextNode("Yippee!");
element.appendChild(antherTextNode);
alert(element.childNodes.length); //2
element.normalize(); //调用合并方法
alert(element.childNodes.length); //1
alert(element.firstChild.nodeValue); //"Hello world!Yippee!"
</script>
浏览器解析文档时永远不会创建相邻的文本节点。
(在某些情况下,在IE6中使用normalize()
方法会导致浏览器崩溃)
分割文本节点
与上述能够合并文本节点的normalize()
方法相对的,Text类型有一个分割文本节点的方法——splitText()
,这个方法传入一个分割位置,即按照指定的位置分割nodeValue
值。原来的文本节点包含从开始到指定位置之前的内容,返回的新文本节点包含剩下的值。示例:
<script type="text/javascript">
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(textNode.data); //"Hello"
alert(element.childNodes.length); //2
</script>
分割文本节点是从文本节点中提取数据的一种常用DOM解析技术。
10.1.5 Comment类型
注释在DOM中是通过Comment类型来表示的。Comment节点有以下特征:
nodeType
的值为8nodeName
的值为“#comment”nodeValue
的值是注释内容parentNode
可能是Document和Element- 不支持(没有)子节点
Comment类型和Text类型继承自相同的基类,因此它拥有除splitText()
方法之外的所有字符串操作方法。与Text类型类似,也可以通过nodeValue
和data
属性来取得注释的内容。注释节点可以通过其父节点来访问。示例:
<body>
<div id="myDiv"><!--A Comment--></div>
</body>
<script type="text/javascript">
var div = document.getElementById("myDiv");
var comment = div.firstChild;
alert(comment.data); //"A Comment"
</script>
还可以使用document.createComment()
方法创建一个注释节点,方法参数为注释文本。示例:
<script type="text/javascript">
var comment = document.createComment("A Comment");
var div = document.getElementById("myDiv");
div.appendChild(comment);
</script>
开发人员很少会创建和访问注释节点,因为注释节点对算法鲜有影响。此外,浏览器也不会识别位于</html>
标签后面的注释。如果要访问注释节点,一定要保证该注释节点是<html>
元素的后代。
10.1.6 CDATASection类型
CDATASection类型只针对基于XML的文档,表示的是CDATA区域。与Comment类似,CDATASection类型继承自Text类型,因此具有除splitText()
方法之外的所有字符串操作方法。CDATASection节点具有以下特征:
nodeType
的值为4nodeName
的值为“#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区域,只需向方法中传入节点的内容即可。
10.1.7 DocumentType类型
DocumentType类型在web浏览器中并不常用,仅有Firefox、Safari和Opera支持它(Chrome4.0也支持了)。DocumentType包含着与文档doctype有关的所有信息,它具有以下特征:
nodeType
的值为10nodeName
的值为doctype的名称nodeValue
的值为nullparentNode
是Document- 不支持(没有)子节点
在DOM1级中,DocumentType对象不能动态创建,而只能通过解析文档代码的方式来创建。支持它的浏览器会将DocumentType对象保存在document.doctype
中。DOM1级描述了DocumentType对象的三个属性:name、entities和notactions。其中,name表示文档类型的名称;entities是由文档类型描述的实体的NamedNodeMap对象;notations是由文档类型描述的符号的NamedNodeMap对象。通常,浏览器中的文档实用的都是HTML或者XHTML文档类型,因此entities和notations都是空列表(列表中的项来自行内文档类型声明)。但不管怎样,只有name属性是有用的。这个属性保存的是文档类型的名称,也就是出现在<!DOCTYPE
之后的文本。示例:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>Title</title>
</head>
<body>
<script type="text/javascript">
alert(document.doctype.name); //"html"
</script>
</body>
</html>
示例中创建了一个严格型HTML4.01文档类型声明,DocumentType中name属性的值就是“HTML”,但是输出却是小写的“html”(没有找到具体的原因,但是只有H5的<!DOCTYPE
之后是“html”)。
(IE的早期版本不支持DocumentType,因此document.doctype
的值始终都是null。可是,这些浏览器会将文档类型声明错误的解释为注释,并且为其创建一个注释节点。IE9会为document.doctype
赋正确的对象,但任然不支持访问DocumentType类型。)
10.1.8 DocumentFragment类型
在所有的节点类型当中,只有DocumentFragment在文档中没有对应的标记。DOM规定文档片段(document fragment)是一种“轻量级”的文档,可以包含和控制节点,但不会像完整的文档那样占用额外的资源。DocumentFragment节点具有下列特征:
nodeType
的值为11nodeName
的值为”#document-fragment”nodeValue
的值为nullparentNode
是null- 子节点可以是Element、ProcessingInstruction、Comment、Text、CDATASection或EntityReference。
虽然不能直接将文档片段直接添加到文档中,但可以将它作为一个“仓库”来使用,即可以在里面保存将来可能会添加到文档中的节点。创建示例:
var fragment = document.createDocumentFragment();
文档片段继承了所有Node的所有方法,通常用于执行那些针对文档的DOM操作。如果将文档中节点添加到文档片段中,被添加的节点将会在文档树中移除,在浏览器中也不会再看到该节点。添加到文档片段中的节点同样也不属于文档树。可以通过appendChild()
和insertBefore()
将文档片段中的内容添加到文档树中。使用这两个方法时,参数是DocumentFragment类型,但是其实现原理是将文档片段中所有子节点添加到相应位置上;文档片段本身永远不会成为文档树的一部分。示例:
<body>
<ui id="myList"></ui>
</body>
<script type="text/javascript">
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);
}
alert(fragment.childNodes.length); //3
ul.appendChild(fragment);
alert(fragment.childNodes.length); //0
</script>
简单的逻辑不需要特别说明,但是有一点:将文档片段中所有子节点添加到相应位置上(同时原本属于文档片段的所有子节点都会在被添加到文档树中后从文档片段中删除),文档片段本身永远不会成为文档树的一部分。这也是代码中两次弹窗的结果不一样的原因。
10.1.9 Attr类型
元素的特性在DOM中以Attr类型来表示。从技术角度讲,特性就是存在于元素的attributes
属性中的节点。特性节点具有以下特征:
nodeType
的值为11nodeName
的值是特性的名称nodeValue
的值是特性的值parentNode
的值为null- 在HTML中不支持(没有)子节点
- 在XML中子节点可以是Text或EntityReference
尽管特性节点也是节点,但是却不被认为是DOM文档树的一部分。
Attr对象有三个属性:name、value和specified。name是特性名称(与nodeName
的值相同),value是特性的值(与nodeValue
的值相同),而spe cified是一个布尔值,用以区别特性是在代码中指定的还是默认的。为某个元素添加一个特性,示例:
<body>
<div id="element">MyDiv</div>
</body>
<script type="text/javascript">
var element = document.getElementById("element");
var attr = document.createAttribute("align");
attr.value = "left";
element.setAttributeNode(attr); //将新创建的特性添加到元素中
alert(element.attributes.getNamedItem("align").value); //letf
alert(element.getAttributeNode("align").value); //left
alert(element.getAttribute("align")); //left
</script>
不建议使用特性节点访问和设置特性,使用getAttribute()
、setAttribute()
和removeAttribute()
更加简便。
10.2 DOM操作技术
10.2.1 动态脚本
动态脚本:动态脚本是指在页面加载时不存在,但将来的某一时刻通过修改DOM动态的添加脚本。跟操作HTML元素一样,创建动态脚本也有两种方式:插入外部文件和直接插入JavaScript代码。动态加载的脚本可以立即执行。示例:
<script type="text/javascript" src="client.js"></script>
使用一个函数动态加载某个外部js文件:
<script type="text/javascript">
function loadScript(url) {
var script = document.createElement("script");
script.type = "text/javascript";
script.url = url;
document.body.appendChild(script);
}
//调用上述方法动态加载一个外部的js文件
loadScript("client.js");
</script>
加载完成后,就可以在页面中其他地方使用这个脚本了,问题只有一个,怎么知道脚本加载完成呢?遗憾的是,并没有什么标准方式来探知这一点。不过,与此相关的一些事件可以派上用场,但要取决于使用的浏览器。
另一种指定JavaScript代码的方式是行内方式(直接插入行内)。示例:
<script type="text/javascript">
function sayHi() {
alert("Hi!");
}
</script>
理论上讲,下面的代码是有效的:
var script = document.createElement("script");
script.type = "text/javascript";
script.appendChild(document.createTextNode("function sayHi() {alert('Hi!');}"));
document.body.appendChild(script);
上述代码可以在Chrome、Firefox、Safari、Opera中正确运行,但是在IE中却不可以,因为IE中将<script>
元素视为特殊的元素,不允许DOM访问子节点,可以使用<script>
元素的text
属性来指定元素中包含的JavaScript代码。示例:
var script = document.createElement("script");
script.type = "text/javascript";
//使用<script>元素的text属性
script.text = "function sayHi() {alert('Hi!');}";
document.body.appendChild(script);
为了兼容早期版本的Safari(主要是Safari3.0以前),可以使用下列代码:
<script type="text/javascript">
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);
</script>
分析过程:首先尝试标准的DOM文本节点方法,因为除了IE之外(在IE中会导致错误),所有的浏览器都支持这种方式。如果这行代码抛出错误,那么说明浏览器是IE,可以使用text
属性。整个过程可以表示为下列函数:
<script type="text/javascript">
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!');}");
</script>
实际上,这种方法与在全局作用域中把相同的字符串传递给eval()
中是一样的。
10.2.2 动态样式
能够把CSS样式包含到HTML页面中的元素有两个。其中,<link>
元素用于包含来自外部的文件,而<style>
元素用于指定嵌入的样式。与动态脚本类似,所谓动态样式就是指在页面刚加载时不存在的样式;动态样式是在页面加载完成后动态添加到页面中的。
示例:典型的<link>
元素:
<link rel="stylesheet" href="styles.css" type="text/css">
使用DOM代码可以很容易的创建出上面的这个元素:
<script type="text/javascript">
var link = document.createElement("link");
link.rel = "stylesheet";
link.type ="text/css";
link.href = "style.css";
var head = document.getElementsByTagNameNS("head")[0];
head.appendChild(link);
</script>
注意:必须将<link>
元素添加到<head>
而不是<body>
中。
上面的整个过程可以使用一个函数来表示:
<script type="text/javascript">
function loadStyles(url) {
var link = document.createElement("link");
link.rel = "stylesheet";
link.type ="text/css";
link.href = url;
var head = document.getElementsByTagNameNS("head")[0];
head.appendChild(link);
}
//调用上面的方法
loadStyles("style.css");
</script>
加载外部样式文件的过程是异步的,也就是加载样式和执行JavaScript代码的过程没有固定的次序。在第13章中可以学到利用事件来检测外部样式文件是否加载完成。
另一种定义样式的方式是嵌入式CSS:
<style type="text/css">
body {
background-color: red;
}
</style>
和上面的引入外部样式一样,下列代码应该是有效的:
<script type="text/javascript">
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);
</script>
在五大主流浏览器中,只有IE会在执行上述代码时报错,IE将<style>
视为一个特殊的、和<script>
类似的节点,不允许访问其子节点,解决办法就是在IE中该节点有一个styleSheet
属性(貌似已经废弃),该属性又有一个cssText
属性可以接受CSS代码。示例:
<script type="text/javascript">
var style = document.createElement("style");
style.type = "text/css";
try {
style.appendChild(document.createTextNode("body{background-color: red;}"));
} catch (ex) {
//貌似styleSheet已经废弃
//style.styleSheet.cssText = "body{background-color: red;}";
style.cssText = "body{background-color: red;}"; //正确执行
}
var head = document.getElementsByTagName("head")[0];
head.appendChild(style);
</script>
一个通用的解决方案如下:
<script type="text/javascript">
function loadStyleString(css) {
var style = document.createElement("style");
style.type = "text/css";
try {
style.appendChild(document.createTextNode(css));
} catch (ex) {
style.cssText = css;
}
var head = document.getElementsByTagName("head")[0];
head.appendChild(style);
}
//调用上面的方法
loadStyleString("body {background-color: red;}");
</script>
补充:在IE中重用同一个<style>
元素并再次设置styleSheet.cssText
属性时,会导致浏览器崩溃。
10.2.3 操作表格
为了构建表格,HTML DOM为<table>
、<tbody>
、<tr>
元素添加了一些属性和方法。
以下是为元素添加的属性和方法:
- 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集合中指定的位置插入一个单元格,返回对新插入单元格的引用
使用上述API,可以简化DOM中对表格的操作,例如,现在有一个表格如下:
<body>
<table border="1" width="100%">
<tbody>
<tr>
<td>1</td>
<td>2</td>
</tr>
<tr>
<td>3</td>
<td>4</td>
</tr>
</tbody>
</table>
</body>
使用上述API插入这个表格:
<script type="text/javascript">
//创建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("1"));
tbody.rows[0].insertCell(1);
tbody.rows[0].cells[1].appendChild(document.createTextNode("2"));
//创建第二行
tbody.insertRow(1);
tbody.rows[1].insertCell(0);
tbody.rows[1].cells[0].appendChild(document.createTextNode("3"));
tbody.rows[1].insertCell(1);
tbody.rows[1].cells[1].appendChild(document.createTextNode("4"));
//将上述表格添加到文档主体中
document.body.appendChild(table);
</script>
10.2.4 使用NodeList
理解NodeList及其“近亲”NamedNodeMap和HTMLCollection,是从整体上透彻理解DOM的关键所在。这三个集合都是“动态”的。每当文档结构发生变化时,它们都会得到更新,因此,它们始终都保存着最新、最准确的信息。从本质上说,所有NodeList对象都是在访问DOM文档时实时运行的查询。下列就是一个会导致无线循环的代码:
<script type="text/javascript">
var divs = document.getElementsByTagName("div"),
i,
div;
for (i=0;i<divs.length;i++) {
div = document.createElement("div");
document.body.appendChild(div);
}
</script>
上述代码中,由于循环的判断条件中使用了NodeList对象,因此每次执行到此(判断)时,都会对文档运行一次查询,效率比较低。这样做的结果就是循环体中不断向body中添加div元素,因此divs.length不断增长,i也不断增长,成为一个死循环。解决方法如下:
<script type="text/javascript">
var divs = document.getElementsByTagName("div"),
len,
i,
div;
for (i=0,len=divs.length;i<len;i++) { //将NodeList在某一时刻的length属性保存到另一个变量中
div = document.createElement("div");
document.body.appendChild(div);
}
</script>
解决方法就是将某一时刻的length属性保存到另一个变量中,这种方式更加保险。
一般来说,应该尽量减少访问NodeList,因为每访问一次NodeList就会对文档进行一次查询。
10.3 小结
理解DOM的关键,就是理解DOM对性能的影响。DOM操作往往是JavaScript程序中开销最大的部分,而因访问NodeList导致的问题为最多。