《JavaScript高级程序设计》第十章,DOM。
DOM(文档对象模型)是针对HTML和XML文档的一个API。
节点层次
DOM可以将任何HTML或XML文档描绘成一个由多层节点构成的结构。每个文档都只有一个根节点,叫做文档节点。每一段标记都可以通过树的一个节点表示:HTML元素用元素节点,特性(attribute)用特性节点,文档类型用文档节点,注释用注释节点,总共有12种节点类型,它们都继承自一个基类型。举个例子:
1 | <html> |
Node类型
DOM1级定义了一个Node接口,该接口由DOM中的所有节点类型实现,该接口在JavaScript中作为Node类型实现。JavaScript中的所有节点类型都继承自Node类型。在除IE外的所有浏览器中都可以访问到这个类型。
属性
- nodeType
- 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
每个节点都有nodeType属性,表明节点的类型,由上述12个数值常量表示。通过和以上常量的比对可以知道节点的类型。由于IE中未公开Node类型的构造函数,所以在IE中只能通过数值的比较来判断。
1 | // 适用于除IE外浏览器 |
nodeName和nodeValue
俩属性的值取决于节点的类型,如对于元素节点来说,nodeName是元素的标签名,nodeValue始终为null。ownerDocument
指向表示整个文档的文档节点。这种关系意为任何节点都属于它所在的文档,任何节点都不能同时存在于多个文档中。
节点关系
- childNodes
每个节点都有一个childNodes属性,其中保存着一个NodeList对象。NodeLists是一种类数组对象,但不是Array的实例。NodeLists实际上是基于DOM结构动态执行查询的结果,因此DOM结构的变化能够自动反映在NodeLists对象中。下面展示对其的访问:
1 | var firstChild = someNode.childNodes[0]; |
如果向将其转为Array,可以使用Array.prototype.slice()
,不过在IE8以及更早版本上,NodeLists是COM对象,因此我们不能对其使用JScript对象的方法,必须手动枚举。
1 | // 适用于除IE8之前外浏览器 |
- firstChild
- lastChild
- previousSibling
- nextSibling
- parentNode
- hasChildNodes()
以上在边界情况下值均为null,如第一个子节点的previousSibling、最后一个子节点的nextSibling等。
节点操作
因为关系指针都是只读的,所以DOM提供了一些操作节点的方法。
appendChild()
接收一个参数:要添加的节点,返回之。如果传入的节点已为文档一部分,则结果为将该节点从原位置转移到新位置。insertBefore()
接收两个参数:要插入的节点、作为参照的节点,返回被插入的节点。如果参照的节点是null,则此方法和appendChild()执行相同操作。replaceChild()
接收两个参数:要插入的节点、被替换的节点,返回被替换的节点。过程为新节点的所有关系指针由旧节点复制而来,技术上旧节点仍存在于文档中,但它在文档中已无自己的位置。removeChild()
接收一个参数:要移除的节点,返回之。同样事实上仍存在但已无自己的位置。cloneNode()
用于创建调用该方法的节点的副本,接收一个布尔值参数,表示是否执行深复制,返回副本。传入true时深复制,即复制节点及其整个子节点树;传入false时浅复制,即只复制节点本身。该方法不会复制添加到DOM节点中的JavaScript属性,如事件处理器等。副本属于文档所有,但并没有为其指定父节点,不过可以通过使用appendChild()
等方法为其指定关系。normalize()
用于处理文档树中的文本节点。由于一些原因可能会出现文本节点不包含文本、连续出现两个文本节点的情况,当某个节点调用此方法时,可以在该节点的后代节点中查找上述两种情况。处理方法为对空文本节点进行删除,对相邻文本节点进行合并。
Document类型
JavaScript用Document类型表示HTML页面或其他基于XML的文档。浏览器中的document对象是继承自Document的HTMLDocument的实例,也是window对象的一个属性。
属性
nodeType:9
nodeName:”#document”
nodeValue:null
parentNode:null
childNodes:DocumentType、Element、ProcessingInstruction、Comment
ownerDocument:null
documentElement
指向HTML页面的<html>元素。body
指向HTML页面的<body>元素。doctype
指向HTML页面的<!DOCTYPE>元素。不同浏览器对其的支持不一致,不赘述。
* document特有属性
document对象有一些标准Document对象没有的属性,它们提供了document对象所表现的网页的一些信息,包括:
- document.title:标题
- document.URL:地址栏中的完整URL
- document.domain:页面域名
- document.referrer:链接到当前页面的那个页面的URL
document对象还有一些特殊集合,它们都是HTMLCollection对象,包括:
- document.anchors:所有带name特性的<a>元素
- document.applets:所有<applet>元素
- document.forms:所有<form>元素
- document.images:所有<img>元素
- document.links:所有带href特性的<a>元素
查找操作
getElementById()
大小写敏感,如果有多个相同id的元素则返回文档中第一个元素。getElementByTagName()
大小写不敏感,返回包含零或多个元素的NodeList。在HTML文档中则返回一个HTMLCollection对象。若传入*则会返回所有元素。
* HTMLCollection
作为一个动态集合,它于NodeList很类似,因此也可以用下标和item()
方法。除此之外还可以使用方括号和namedItem()
方法。
1 | var images = document.getElementByTagName("img"); |
*document特有操作getElementByName()
会返回带有给定name特性的所有元素的一个HTMLCollection。
写入操作
document有将输入流写入网页的能力,具体来说是使用以下方法:write()
、writeln()
、open()
、close()
。前两者均接收一个字符串,前者会原样写入,后者会在字符串末尾添加换行符。在页面被加载时,它们可以向页面中动态地加入内容。
1 | <html> |
该例子使用document.write()
在页面被呈现的过程中直接向其中输出了内容。但如果在文档结束后再调用document.write()
,那么输出的内容将重写整个页面。
1 | <html> |
Element类型
Element类型用于表现XML或HTML元素,提供对元素标签名、子节点和特性的访问。
属性
- nodeType:1
- nodeName:元素的标签名
- nodeValue:null
- parentNode:Document、Element
- childNodes:Element、Text、Comment、ProcessingInstruction、CDATASection、EntityReference
- tagName:元素的标签名,为全大写,与nodeName相等
要注意不同浏览器的区别,举个例子:
1 | <ul id="myList"> |
对如上的代码,大部分浏览器解析的结果为:<ul>元素包含3个子节点,分别是3个<li>元素;IE浏览器解析第结果为:<ul>元素包含7个子节点,分别是3个<li>元素和4个文本节点(元素之间的空白符)。因此如果是要遍历子节点,一定不要忘了浏览器间的差异。
- attributes
attributes属性中包含一个NamedNodeMap,与NodeList,是一个动态的集合。元素的每个属性都用一个Attr节点表示,并被保存在NamedNodeMap对象中,节点的nodeName为特性名,nodeValue为特性值。NamedNodeMap对象拥有以下方法:getNamedItem(name)
、removeNamedItem(name)
、setNamedItem(node)
、item(pos)
。分别用来获取、移除、添加、返回pos位置的节点。
1 | var id = element.attributes.getNamedItem("id").nodeValue; |
* HTML元素特有属性
所有HTML元素都由HTMLElement类型标识,HTMLElement类型继承了Element类型,并添加了一些属性:
- id:元素id
- title:元素附加说明信息,一般通过工具提示条显示
- lang:元素内容的语言代码
- dir:语言的方向,值为”ltr”(left-to-right)、”rtl”(right-to-left)
- className:元素的class
特性操作
每个元素都有一个或多个特性,用于给出相应元素或其内容的附加信息。
- getAttribute()
1 | <div id="myDiv" class="bd" title="Body text" lang="en" dir="ltr"myAttr="hello"></div> |
1 | var div = document.getElementById("myDiv"); |
注意传递的特性名与实际的特性名相同,因此要得到class特性值应该用getAttribute("class")
而非getAttribute("className")
。特性的名称是大小写不敏感的。还可以像例子一样自定义特性,不过根据HTML5规范,自定义特性应加上data-前缀;且只有公认的特性才会以属性的形式添加到DOM对象中。有两类特殊的特性,它们虽然有对应的属性名,但属性的值与通过getAttribute()
返回的值的类型不一样,它们是style特性和诸如onclick这样的事件处理特性,二者属性值均为对象,但getAttribute()
返回的值则为字符串。
- setAttribute()
接收两个参数:要设置的特姓名、值。如果特性已存在则替换,如果特性不存在则创建该属性并设置相应的值。
1 | div.setAttribute("id", "someOtherId"); |
通过该方法设置的特姓名会被统一转换为小写形式。注意如果像后面那样为DOM元素添加一个自定义的属性,该属性不会自动成为元素的特性。
removeAttribute()
用于彻底删除元素的特性,即不仅删除特性值,还会彻底删除特性。getAttributeNode()
这个返回的是Attr类型的节点,不过不建议操作节点,直接用以上方法更方便。setAttributeNode()
这个返回的是Attr类型的节点,不过不建议操作节点,直接用以上方法更方便。
创建操作
可以使用document.createElement()
方法创建新元素,接收一个参数:要创建的元素的标签名。不过新元素尚未被添加到文档树中,可以使用appendChild()
等来将其进行添加。
1 | var div = document.createElement("div"); |
在IE中可以使用另一种传参模式,接收一个参数:完整的元素标签。
1 | var div = document.createElement("<div id=\"myNewDiv\ class=\"box\""></div>"); |
Text类型
为文本节点,包含纯文本内容。可包含转义后的HTML字符,但不能包含HTML代码。
属性
- nodeType:3
- nodeName:”#text”
- nodeValue:包含的文本
- parentNode:Element
- childNodes:无
- data:包含的文本
- length:节点中字符数目
文本操作
appendData(text)
将text添加到节点末尾。deleteData(offset, count)
从offset指定的位置开始删除count个字符。insertData(offset, text)
从offset指定的位置插入text。replaceData(offset, count, text)
用text替换从offset指定的位置开始到offset+count为止处的字符。substringData(offset, count)
提取从offset指定的位置开始到offset+count为止处的字符。splitText(offset)
从offset指定的位置将文本节点分成两个文本节点。原节点的文本变为前部分的字符,新节点的文本为后部分的字符,返回新节点。normalize()
与splitText()
效果相反,不接收参数。在一个包含两个或多个文本节点的父元素上调用此方法,可以将所有文本节点合并为一个节点,结果节点的nodeValue等于合并前每个文本节点的nodeValue值拼接起来的值。
创建操作
可以使用document.createTextNode()
方法创建文本节点,接收一个参数:要插入节点中的文本。不过新节点尚未被添加到文档树中,可以使用appendChild()
等来将其进行添加。
1 | // 其实文本会转为"<strong>Hello</strong>world!" |
Comment类型
为注释,和Text类型继承自相同的基类,因此拥有除了splitText()
之外的所有字符串操作方法。
属性
- nodeType:8
- nodeName:”#comment”
- nodeValue:注释的内容
- parentNode:Document、Element
- childNodes:无
- data:注释的内容
创建操作
可以使用document.createComment()
方法创建注释节点。
CDATASection类型
CDATASection类型只针对基于XML的文档,表示的是CDATA区域。它继承自Text类型,因此拥有除了splitText()
之外的所有字符串操作方法。
属性
- nodeType:4
- nodeName:”#cdata-section”
- nodeValue:CDATA区域的内容
- parentNode:Document、Element
- childNodes:无
创建操作
可以使用document.createCDATASection()
方法创建CDATA区域。
DocumentType类型
包含着与文档的doctype有关的所有信息。在DOM1级中,DocumentType对象不能动态创建,而只能通过解析文档代码的方式来创建。
属性
- nodeType:10
- nodeName:doctype的名称
- nodeValue:null
- parentNode:Document
- childNodes:无
- name:文档类型的名称
- entities:文档类型描述的实体的NamedNodeMap对象
- notations:文档类型描述的符号的NamedNodeMap对象
DocumentFragment类型
在所有节点类型中,只有DocumentFragment在文档中没有对应的标记。DOM将其规定为一种轻量级的文档,可以包含和控制节点,但不像完整文档一样占用额外的资源。
属性
- nodeType:11
- nodeName:”#document-fragment”
- nodeValue:null
- parentNode:null
- childNodes:Element、ProcessingInstruction、Comment、Text、CDATASection、EntityReference
创建操作
可以使用document.createDoumentFragment()
方法创建文档片段。虽然不能将文档片段直接添加到文档中,但可以将其作为一个仓库来使用,即可以在里面保存将来可能会添加到文档中的节点。不过新节点尚未被添加到文档树中,可以使用appendChild()
等来将其进行添加。对于文档片段来说,添加的过程实际上是将文档片段的所有子节点添加到相应位置并从文档片段中删除,而文档片段本身并不会成为文档树的一部分。
1 | var fragment = document.createDoumentFragment(); |
如果我们想添加多个元素,但又不想要一个个地添加而产生的反复渲染,可以使用文档片段。举个例子:
1 | <ul id="myList"></ul> |
1 | // 向其中添加三个列表项 |
Attr类型
为元素的特性,从技术角度讲,特性就是存在于元素的attributes属性中的节点。
属性
- nodeType:2
- nodeName:特性的名
- nodeValue:特性的值
- parentNode:null
- childNodes:在HTML中无;在XML中可以为Text或EntityReference
- name:特性的名
- value:特性的值
- specified:布尔值,用于区别特性是在代码中指定的还是默认的
创建操作
可以使用document.createAttribute()
方法创建特性节点。若想将新创建的特性添加到元素中,必须使用元素的setAttributeNode()
方法。
1 | var attr = document.createAttribute("align"); |
DOM操作技术
我们知道,很多时候我们可以借助DOM做一些和浏览器交互的操作。
动态脚本
动态脚本指的是,在页面加载时不存在,但将来的某一时刻通过修改DOM动态添加的脚本。有两种创建方式:插入外部文件、直接插入JavaScript代码。
1 | // 插入外部文件 |
动态样式
动态样式指的是,在页面加载时不存在,加载完成后动态添加到页面中的样式。有两种创建方式:插入外部文件(用<link>的)、直接插入代码(用用<style>的)。
1 | // 插入外部文件 |
操作表格
<table>元素是HTML中最复杂的结构之一,表格的创建一般都必须涉及表格行、单元格、表头等方面的标签,用DOM方法创建就会很复杂。为了方便,DOM为<table>、<tbody>、<tr>元素添加了一些方法。
为table添加的属性和方法:
- caption:指向<caption>元素的指针
- tBodies:一个<tbody>元素的HTMLCollection
- tFoot:指向<tFoot>元素的指针
- tHead:指向<tHead>元素的指针
- rows:表格中所有行的HTMLCollection
- createTHead()
- createTFoot()
- createCaption()
- deleteTHead()
- deleteTFoot()
- deleteCaption()
为tbody添加的属性和方法:
- deleteRow(pos)
- insertRow(pos)
为tr添加的属性和方法:
- cells:tr中所有单元格的HTMLCollection
- deleteCell(pos)
- insertCell(pos)
举个例子,如想创建一个表格,用原始的DOM操作(左)和用DOM提供的便捷操作(右):
1 | <table> |
使用NodeList
上文提到了三个动态的集合:NodeList、NamedNodeMap、HTMLCollection,每当文档结构发生变化时,它们都会得到更新。本质上说,所有以上三者的对象都是在访问DOM文档时实时运行的查询。因此会遇到一些问题,举例如下:
1 | var divs = document.getElementByTagName("div"), i, div; |
这里会进入一个死循环,因为每次循环都要对length求值,也就是所有div元素的查询。但每次进入循环后都会创建一个新的div元素并添加入文档,因此divs.length会在每次循环后递增。i和divs.length同步递增,因此它们永远不会相等,是一个死循环。解决办法:保存最初的divs.length的快照。
一般来说要减少对它们的访问,因为每次访问三种动态集合,都会运行一次基于文档的查询,建议是将值缓存起来。