DOM,文档对象模型,是一个针对HTML和XML文档的一个API,它描绘了一个层次化的节点树。值得注意的是IE中的所有DOM对象都是使用COM对象实现的,这就造成了IE中的DOM对象与原生JS对象表现并不一致。
节点层次
文档节点是每个文档的根节点。在html文档中,文档节点只有一个子节点即html元素。这个元素我们称之为文档元素,每个文档只有一个文档元素,其他所有元素都包含在文档元素中。
节点分为几种不同的类型,每种类型分别表示文档中不同的信息及标记。
比如:元素节点,特性节点,文档节点,注释节点等。总共有12种节点类型,这些类型都继承自一个基类型。
Node类型
这是DOM1级定义的一个接口,DOM中所有类型节点都会实现这个接口。在JS中作为Node类型实现,所有节点类型皆继承自Node类型。不过IE直接访问不到Node。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)
var htmlNode = document.getElementsByTagName("html")[0];
//本来标准的写法应该是与Node.ELEMENT_NODE这个常量做比较,但是由于IE访问不到Node这个类型,所以只好直接比较
if (htmlNode.nodeType == 1){
alert("Node is an element.");
alert(htmlNode.nodeName); //HTML
alert(htmlNode.nodeValue); //null
}
这里查找html元素时使用的方法返回的是一个NodeList对象,基本对node类型的查找和获取都会涉及到这个对象。这个对象是一个类数组对象,许多使用方法都和数组是一样的,但是它并不是Array类型的实例。其特别的地方在于它其实是基于DOM结构动态执行查询的结果,DOM结构的变化可以自动反应在NodeList中。想将NodeList转换为数组,可以使用Array.prototype.slice()方法。
var bodyNode = document.getElementsByTagName("body")[0];
var childList = bodyNode.childNodes;
var childListArray = Array.prototype.slice.call(childList,0);
alert(childList.length); //8
alert(childListArray.length); //8
bodyNode.removeChild(bodyNode.lastChild);
alert(childList.length); //7
alert(childListArray.length); //8
节点关系
就是父元素与子元素咯。
bodyNode.childNodes;
bodyNode.parentNode;
bodyNode.previousSibling;
bodyNode.nextSibling;
bodyNode.firstChild;
bodyNode.lastChild;
bodyNode.hasChildNodes();
bodyNode.ownerDocument;
不是每种节点都有子节点哦,有的不能有子节点的。
操作节点
appendChild()用于向childNodes列表结尾添加一个节点。如果传入到这个方法中的节点已经是文档的一部分了,那么该节点就会被移动到新节点。任何DOM节点不能同时出现在多个位置。
//someNode 有多个节点
var returnedNode = someNode.appendChild(someNode.firstChild);
alert(returnedNode == someNode.firstChild); //false
alert(returnedNode == someNode.lastChild); //true
insertBefore()
var returnedNode = someNode.insertBefore(newNode, someNode.firstChild);
alert(returnedNode == newNode); //true
alert(newNode == someNode.firstChild); //true
replaceChild()
在替换的过程中,该节点的所有关系指针都会从被它替换的节点复制过来,被替换的节点还在文档中,但是失去了自己的位置。
var returnedNode = someNode.replaceChild(newNode, someNode.firstChild);
removeChild()
同样的,被移除的节点还是归文档所有,没有位置了。
var formerFirstChild = someNode.removeChild(someNode.firstChild);
cloneNode()
这个方法接受一个布尔值表示是否进行深复制,深复制就是复制节点及其子节点树。要注意的是,复制后的节点没有父节点,是孤独的节点。。需要使用上面提到的方法添加。
normalize()
这个方法是处理文档树中的文本节点。
Document类型
document是window的一个属性,是Document的实例。
- nodeType:9
- nodeName:"#document"
- nodeValue:null
- parentNode:null
- ownerDocument:null
- 其子节点可以是:DocumentType(最多一个)、Element(最多一个)、ProcessingInstruction、Comment
文档的子节点
document.documentElement//指向html元素
document.body//指向body元素
document.doctype;//指向<!DOCTYPE>
文档信息
document.title = "New page title";
document.URL;
document.domain;
document.referrer;//来自哪个页面的URL
其中只有title和domain是可写的。
domain对写入的值是有限制的,只能写入当前域的父域。
//来自p2p.wrox.com
document.domain = "wrox.com"; //成功
document.domain = "nczonline.net"; //报错
document.domain = "p2p.wrox.com "; //报错
对于包含来自其他子域框架的页面这个很有用,因为来自不同域的页面不能通过JavaScript通信,所以通过将domain设置为同一个父域,就可以实现互相访问对方的JS对象。
查找元素
//这个返回一个元素
document.getElementById("myDiv");
//这个返回元素列表HTMLCollectioin
document.getElementsByTagName("img");
//找到name属性符合的元素。这个返回元素列表HTMLCollectioin
document.getElementsByName("color");
var ul = document.getElementById("myList");
//可以直接在元素里查找
var items = ul.getElementsByTagName("li");
一些特殊集合:
document.anchors
document.applets
document.forms
document.images
document.links
DOM一致性检测
用来检测浏览器实现了DOM的哪些部分。DOM1提供了一个方法。将要检测的功能和版本号传入。
var hasXmlDom = document.implementation.hasFeature("XML", "1.0");
不过这个方法并不保险,有的浏览器对没实现的功能也返回true,所以建议对不确定的直接进行能力检测。
文档写入
将输出流写入到网页中的功能。有4个方法:write()、writeln()、open()、close()。
<html>
<head>
<title>document.write() Example 3</title>
</head>
<body>
<script type="text/javascript">
document.write("<strong>" + (new Date()).toString() + "</strong>");
</script>
</body>
</html>
在文档加载过程中使用,则会加载内容到当前位置,在文档加载结束后再执行则会覆盖掉整个文档
Element类型
Element就是我们最常用的元素
- nodeType:1
- nodeName:元素的标签名
- nodeValue:null
- parentNode:Document或Element
- ownerDocument:Document
- 其子节点可以是:Element、Text、Comment、ProcessingInstruction、CDATASection、EntityReference
HTML元素
所有HTML元素都是HTMLElement或其子类。HTMLElement直接继承自Element。有这么几个通用属性:
//<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"
取得特性
getAttribute()、setAttribute()、removeAttribute()
alert(div.getAttribute("id"));
alert(div.getAttribute("class"));
alert(div.getAttribute("title"));
alert(div.getAttribute("lang"));
alert(div.getAttribute("data_myOwnAttr"));
alert(div.data_myOwnAttr); //undefined(除IE)
要注意的是,只有公认的特性才能通过属性名的方式来访问,自定义的特性除IE外的浏览器不会为他们创建属性。
对于style这个属性,直接访问属性得到的是一个对象,便于访问各个样式;而getAttribute("style")则返回字符串。
对于onclick这样的事件处理特性,直接访问属性得到的是一个函数,getAttribute()则返回字符串。
所以在访问非自定义属性时推荐使用直接访问元素属性的方法。
设置特性
div.setAttribute("id", "someOtherId");
div.id = "someOtherId";
同样,通过属性来设置自定义特性是无效的。
删除特性
div.removeAttribute("class");
attributes属性
只有Element类型有这个属性。这个属性中包含一个NameNodeMap。元素的每一个特性由一个Attr节点表示,每个节点都保存在NameNodeMap对象中。
//获取特性
var id = element.attributes.getNamedItem("id").nodeValue;
var id = element.attributes["id"].nodeValue;
//修改特性
element.attributes["id"].nodeValue = "someOtherId";
var oldAttr = element.attributes.removeNamedItem("id");
element.attributes.setNamedItem(newAttr);
创建元素
var div = document.createElement("div");
div.id = "myNewDiv";
div.className = "box";
document.body.appendChild(div);
//在IE中也可以这样
var div = document.createElement("<div id=\"myNewDiv\" class=\"box\"></div >");
遍历子节点
<ul id="myList">
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
<ul id="myList"><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>
对于上面的两个ul元素childNodes包含的数目可能不同,IE外的浏览器对第一个会包含3个li和4个文本节点。如果你真的要遍历,你可以在遍历的同时判断下节点的类型。
Text类型
包含纯文本内容
- nodeType:3
- nodeName:"#text"
- nodeValue:文本内容
- parentNode:Element
- ownerDocument:Document
- 无子节点
<div>Hello World!</div>
var textNode = div.firstChild;
//获取或设置文本的值
div.firstChild.nodeValue = "Some other message";
div.firstChild.data = "Some other message";
创建文本节点
var element = document.createElement("div");
element.className = "message";
//这里的大于小于号之类的会被转义,不会被解释为元素标签
var textNode = document.createTextNode("<strong>Hello</strong> 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);
分割文本节点
被分割出来的新节点于原来的有相同的父节点。
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);
其他方法
appendData(text)
deleteData(offset, count)
insertData(offset, text)
replaceData(offset, count, text)
substringData(offset, count)
length
这些方法都是用来获取或修改文本节点中的内容的。
Comment类型
在DOM中的注释通过这个类型来表示
- nodeType:8
- nodeName:"#comment"
- nodeValue:注释文本内容
- parentNode:Document或Element
- ownerDocument:Document
- 无子节点
- 与text类型有相同的基类,所以上面的字符串方法它也有
<div id="myDiv"><!--A comment --></div>
var div = document.getElementById("myDiv");
var comment = div.firstChild;
alert(comment.data);
CDATASection类型
DocumentType类型
DocumentFragment类型
这个类型在文档中没有对应的标记,是一个轻量级的文档,可以包含和控制节点。它不能被直接添加到文档中,但是可以作为一个仓库来使用,在里面保存将来会用到的节点。
- nodeType:11
- nodeName:"#document-fragment"
- nodeValue:null
- parentNode:null
- ownerDocument:Document
- 子节点可以是Element、ProcessingInstruction、Comment、Text、CDATASection、EntityReference
Node的各种DOM节点操作方法文档片段都继承了。
文档片段的一个使用场景就是当我们循环添加节点时,如果直接将节点添加到文档中,那么每次添加都会导致浏览器的重新渲染。如果我们先把要循环的节点添加到一个文档片段中,拼好了再添加到文档中呢,这个问题就解决了。
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类型来表示。从技术角度讲,特性就是存在于元素的attributes中的节点。尽管它们是节点,但不认为它们是DOM文档树的一部分。
var ul = document.getElementById("myList");
//特性节点不会出现在文档树中
alert(ul.childNodes.length);
alert(ul.attributes["class"].value);
//alert(ul.attributes["align"].value);会报错
//添加特性节点
var attr = document.createAttribute("align");
attr.value = "left";
ul.setAttributeNode(attr);
alert(ul.attributes["align"].value);
一般只使用之前提到的getAttribute()、setAttribute()、remveAttribute()方法,很少直接引用特性节点。
DOM操作技术
动态脚本
指的是页面加载时并不存在,但将来的某一时刻通过修改DOM动态添加的脚本。动态添加同样有两种方式,添加外部文件和行内代码。
外部文件:
function loadScript(url){
var script = document.createElement("script");
script.type = "text/javascript";
script.src = url;
document.body.appendChild(script);
}
loadScript("client.js");
脚本加载完成就可以使用了,但是这里我们并不能判断是否加载完成了,需要借助一些事件来判断。
行内代码:
function loadScriptString(code){
var script = document.createElement("script");
script.type = "text/javascript";
try {
//由于IE将script视作一个特殊的元素,不允许DOM访问其子节点,这样在IE中会报错
script.appendChild(document.createTextNode(code));
} catch (ex){
//直接设置text属性有的浏览器不支持,但IE肯定支持
script.text = code;
}
document.body.appendChild(script);
}
loadScriptString("function sayHi(){alert('hi');}");
动态样式
外部文件:
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);
}
loadStyles("styles.css");
内联样式:
IE同样有BUG
function loadStyleString(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}");
操作表格
DOM为表格添加了一些属性和方法,以便我们可以更好的操作表格。
table元素的:
caption
tBodies
tFoot
tHead
rows
createTHead()
createTFoot()
createCaption()
deleteTFoot()
deleteCaption()
deleteRow(pos)
insertRow(pos)
body元素的:
rows
deleteRow(pos)
insertRow(pos)
cells
deleteCell(pos)
insertCell(pos)
使用NodeList
NodeList以及其近亲NamedNodeMap和HTMLCollection都是动态的,也就是说每当文档发生变化时,对他们的查询始终返回最新的信息。这也就导致了一些问题,
var divs = document.getElementsByTagName("div"),
i,
div;
for (i=0; i < divs.length; i++){
div = document.createElement("div");
document.body.appendChild(div);
}
在这段代码中每次增加一个div节点,divs中的元素也会同时增加一个,divs.length永远会比当前的循环大。
// 这样就不会有问题了
var divs = document.getElementsByTagName("div"),
i,
len,
div;
var divtemp = divs;
for (i=0, len=divs.length; i < len; i++){
div = document.createElement("div");
document.body.appendChild(div);
}
这也提醒了我们,使用nodeList要谨慎,因为每次使用都会引起一次查询。