DOM 基础
DOM, Document Object Model. 即文档对象模型。是HTML和XML文档的编程接口。DOM表示由多层节点构成的文档,通过它开发者可以添加、删除和修改页面的各个部分。此文章主要讲解节点类型。其中Node类型、Document类型、Element类型和Text类型比较重要也是比较用的多。DocumentFragment类型可以提高性能。
节点层级
任何HTML或XML文档都可以用DOM表示未一个由节点组成的层级结构。DOM中总共有12种节点类型,这些类型都继承一种基本类型,每种类型对应着文档中不同的信息和(或)标记,也都有自己不同的特性、数据和方法。而且与其他类型有某种关系,这些关系构成了层级,让标记可以表示为一个以特定节点为根的树形结构。
<html>
<head>
<title>Sample Page</title>
</head>
<body>
<p>Hello World</p>
</body>
</html>
以上表示为层级结构就是:
其中,Document节点表示每个文档的根节点。根节点的唯一子节点是<html>
原始,称之为文档元素(documentElement)。文档元素是最外层的元素,所有元素都存在于这个元素之内。而且每个文档只能有一个文档元素。【注意:XML中任何元素都可能成为文档元素,而HTML始终是<html>
】
Node 类型
Node接口在JavaScript中被实现为Node类型,除IE以外都可直接访问此类型。在JavaScript中,所有节点类型都继承Node类型。
nodeType
每个节点都有nodeType
属性,表示该节点的类型。节点类型由定义在Node类型上的12个数值常量表示。
Node.ELEMENT_NODE (1) 元素节点
Node.ATTRIBUTE_NODE (2) 属性节点
Node.TEXT_NODE (3) 文本节点
Node.CDATA_SECTION_NODE (4) CDATA节点
Node.ENTRY_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) DTD声明节点
nodeName和nodeValue
nodeName与nodeValue属性保存着有关节点的信息。这两个属性的值完全取决于节点类型。在使用这两个属性前,最好检测节点类型。如若标签为元素标签,则nodeName显示其标签名,nodeValue则始终为null。
节点关系
节点关系就是文档树的关系,有子节点、父节点、兄弟节点。在JavaScript中,节点属性可以进行访问,类似于C/C++的指针。
childNodes属性
每个节点都有childNodes属性,其中包含一个NodeList的实例(NodeList是一个类数组对象,用于存储可以按位置存取的有序节点,但注意并不是Array的实例)。NodeList的属性与方法如下:
-
访问元素:中括号
[]
或item()
方法let firstchild = someNode.childNodes[0]; let secondchild= someNode.childNodes.item(1);
-
子节点个数:
length
属性let count = someNode.childNodes.length;
注意:<!DOCTYPE html>
也算一个节点
如果学习过《数据结构》,则就很好理解接下来的属性了。
属性 | 说明 | 类比 |
---|---|---|
childNodes | 子节点类数组,NodeList对象 | 子节点数组 |
parentNode | 父节点 | 父节点指针 |
nextSibling | 兄弟节点的下一个 | next指针 |
previousSibling | 兄弟节点的前一个 | prev指针 |
firstChild | 子节点中的第一个节点 | 头指针 |
lastChild | 子节点中的最后一个节点 | 尾指针 |
ownerDocument | 指向代表整个文档的文档节点(document) | 根节点 |
操纵节点
这里操纵节点都是操纵子节点,包括添加节点、删除节点、替换节点。
添加节点
-
appendChild()方法:接受一个参数,用于在childNodes列表末尾添加节点,如果传入已经存在的节点,则该节点会更新到新位置。
let returnedNode=someNode.appendChild(newNode); //传入新节点 someNode.appendChild(exitNode); //传入已经存在的节点
-
insertBefore()方法:接受两个参数,节点和参照节点。将要插入的节点变成参照节点的前一个兄弟节点,并被返回。若是null,则与appendChild方法一致。
删除节点
- removeChild()方法:接受一个参数,用于删除需要移除的节点。并返回被删除的节点。
替换节点
- replaceChild()方法:接受两个参数,插入节点和被替换节点。用于将替换节点删除,并将插入节点插入此位置。返回被替换节点。
注意,一个节点不能同时出现在两个或多个地方,上面四种方法,都相当于操作指针,而不是复制。若无子节点,则报错。
其他方法
cloneNode()
此方法会返回与调用它的节点一摸一样的节点。接受一个布尔值参数,true表示深复制,即复制节点及整个子DOM树;false表示浅复制,即只复制该方法的节点。复制返回的节点属于文档所有,但尚未指定父节点,所以可称为孤儿节点(orphan)。
<ul>
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
</ul>
let deepList=myList.cloneNode(true); //deepList.length=3
let shallowList=mylist.cloneNode(false);//shallowList.length=0
注意:cloneNode()方法不会复制添加到DOM节点的JavaScript属性,比如事件处理程序。这个方法只复制HTML属性,以及可选地复制子节点。除此以外则一概不会复制。
normalize()
此方法主要用于使DOM 树变得更加简洁,在节点上调用normalize()方法会检测这个节点的所有后代,删除空文本节点和合并兄弟节点都为文本节点。【文本节点本身在 DOM 树中不可见,只是它们所包含的文本数据被渲染在浏览器中】
Document 类型
Document类型表示文档节点的类型。文档对象document表示整个HTML页面。document是window对象的属性,因此是一个全局对象。其子节点可以是DocumentType(最多一个)、Element(最多一个)、processingInstruction或Comment类型。
document对象可用于获取关于页面的信息以及操纵其外观和底层结构。
文档子节点
- documentElement属性:始终指向
<html>
元素。 - body属性:直接指向
<body>
元素。因为开发者使用的元素最多就是<body>
,因此这样可以更快访问。 - doctype属性:直接指向
<!DOCTYPE html>
元素。
文档类型是只读的。注释由于浏览器的不同,可能表现不同(即Comment类型)。
文档信息
document作为HTMLDocument的示例,除了拥有标准Document对象上所有的属性,还拥有提供浏览器所加载网页的信息。
- titile属性:包含
<title>
元素中的文本;可读写。 - URL属性:包含当前网页的完整URL
- domain属性:包含页面的域名。但已经舍弃。原可用于不同子域的窗格或内置窗格时的通信。
- referrer属性:包含链接到当前页面的那个页面的URL,如果当前页面没有来源,则包含空字符串。
定位元素
HTMLCollection对象
与NodeList对象一样,都是类数组对象,都是“实时”列表。同样有length属性、访问也可用中括号和item()方法,但它多一个方法,即namedItem(),可通过标签的name属性取得某一项的引用。
属性或方法 | 说明 |
---|---|
length属性 | 元素个数 |
[] | 访问元素,传索引或者传name属性值(相当于调用item()和namedItem()方法) |
item()方法 | 访问元素,传索引 |
namedItem()方法 | 通过标签的name属性取得某一项的引用 |
getElementById()
参数 | 返回值 |
---|---|
要获取元素的ID | 若找到则返回元素的引用,无则返回null,若有多个匹配则返回第一个 |
getElementsByTagName()
参数 | 返回值 |
---|---|
要获取元素的标签名 | 返回0个或多个元素的所组成的HTMLCollection对象 |
getElementsByName()
参数 | 返回值 |
---|---|
要获取元素的name属性值 | 返回具有给定name属性的所有元素,同样是HTMLCollection对象 |
getElementsByClassName()
参数 | 返回值 |
---|---|
要获取元素的类名 | 返回具有给定类名的所有元素,同样是HTMLCollection对象 |
在DOM扩展中,还有Selectors Api可以定位元素(querySelector()、querySelectorAll()、matches())。
注意:对于document.getElementByTagName()方法,虽然规范要求区分标签的大小写,但为了最大限度兼容原有HTML页面,实际上是不区分的。如果是在XML页面(如XHTML)中使用,则要区分大小写。在元素中也可以使用以上方法,不过得到的结果限制在元素之内。
特殊集合
document对象上还暴露了几个特殊集合,这些集合也都是HTMLCollection的实例。这些集合是访问文档中公共部分的快捷方式。
- document.anchors:包含文档中所有带name属性的
<a>
元素。 - document.applets:包含文档纵所有
<applet>
元素(因为<applet>
元素已经不建议使用,所以这个集合已经废弃) - document.forms:包含文档中所有
<form>
元素。 - document.images:包含文档中所有
<img>
元素。 - document.links:包含文档中所有带href属性的
<a>
元素
DOM兼容性检测
由于DOM有多个Level和多个部分,因此确定浏览器实现了DOM的哪些部分是很必要的。
document.implementation属性是一个对象,其中提供了与浏览器DOM实现相关的信息和能力。
document.implementation上有一个hasFeature()方法,接收两个参数:特性名称和DOM版本。但由于实现不一致,因此已经不可靠,其返回值都是true。
文档写入
- write()方法:接收一个字符串参数,将这个字符串写入网页中。
- writeln()方法:与write类似,同样接收一个字符串参数,将这个字符串写入网页中。但会在字符串末尾追加一个换行符。
注意特项
-
通过文档写入的标签同样可以进行操作。与普通标签一样。
-
write()和writeln()方法经常用于动态包含外部资源,如JavaScript文件。但注意字符串中不能直接包含
</script>
,因为这个字符串会被解释为脚本块的结尾,导致后面的代码无法运行。需要替换成<\/scirpt>
。 -
通常情况下是在页面渲染期间向文档中输出内容,这种情况下不会重写整个页面,而是追加。但若是在页面加载完之后再调用则输出内容会重写整个页面。
<script> widow.onload=fuction(){ document.wirte("hello world"); } </script>
以上使用了widow.onload事件处理程序,将document.wirte()函数推迟到页面加载完毕后执行。执行之后,字符串“hello world”会重写整个页面内容。
open()和close()方法分别用于打开和关闭网页输出流。在调用write和writeln时不是必需的。
Element 类型
Element表示XML或HTML元素,对外暴露出访问元素标签名、子节点和属性的能力。可以通过nodeName或tagName属性来获取元素的标签名(返回值一样)。在HTML中,元素标签名始终以全大写表示;在XML(包括XHTML)中,标签名始终与源代码中的大小写一致。如果不确定是HTML还是XML,最好将标签名转化为小写形式再做比较(使用字符串的toLowerCase()方法)。
if(element.nodeName.toLowerCase()==="div"){
//do something
}
HTML 元素
所有HTML元素都通过HTMLElement 类型表示,包括其直接实例和间接实例。HTMLElement直接继承Element 并增加了一些属性,它们是所有HTML 元素上都有的标准属性:
属性名 | 说明 |
---|---|
id | 元素再文档中的唯一标识符 |
title | 包含元素的额外信息,通常以提示条形式展示(鼠标移动至标签位置时展示) |
lang | 元素内容的语言代码,很少用 |
dir | 语言的书写方向,ltr 表示从左到右,rtl 表示从右到左,很少用 |
className | 相当于class属性,用于指定元素的CSS类 |
所有这些属性可以用来获取对应的属性值,也可以用来修改相应的值。
取得、设置、移除属性
与属性相关的DOM方法主要有三个:getAttribute()
、setAttribute()
和removeAttribute()
。这些方法主要用于操纵属性,包括在HTMLElement 类型上定义的属性。
方法 | 参数 | 返回值 | 说明 |
---|---|---|---|
getAttribute() | 属性名(字符串) | 存在返回属性值,不存在返回null | 属性名要与实际属性名一致,如class属性就传"class"而不是"className"。属性可以是正式属性或者自定义属性。 |
setAttribute() | 两个参数:要设置的属性名、属性的值(都是字符串形式) | 无 | 如果属性已经存在则替换原值,若不存在则创建该属性。设置的属性名会规范为小写形式 |
removeAttribute() | 属性名(字符串) | 无 | 不单单是清除属性的值,而是把整个属性完全从元素中去掉 |
注意:
-
属性名不区分大小写,另外根据HTML5规范要求,自定义属性名应该加前缀
data-
以方便验证。 -
DOM对象可直接访问公认的属性,但自定义属性不可以直接访问,而使用getAttribute()方法等进行访问。
-
DOM对象访问的属性中有两个返回的值跟使用getAttribute()取得的值不一样。
属性 DOM对象访问 getAttribute()方法访问 style 返回CSSStyleDeclaration对象 CSS字符串 事件处理程序(或者事件属性) 返回JavaScript函数(未指定函数则返回null) 返回字符串形式的源代码 -
开发者在进行DOM编程时通常会放弃使用
getAttribute()
而只使用对象属性。getAttribute()
主要用于取得自定义属性的值。
attributes属性
Element 类型是唯一使用 attributes 属性的DOM节点类型。 attributes 属性包含一个NamedNodeMap实例,是一个类似 NodeList 的 “实时” 集合。元素的每个属性都表示为一个 Attr节点,并保存在这个 NamedNodeMap 对象中。NamedNodeMap对象包含下列方法:
方法 | 参数 | 返回值 |
---|---|---|
getNamedItem()或[] | 属性名(字符串)name | 返回nodeName属性等于给定属性名name的节点 |
removeNamedItem() | 属性名(字符串)name | 删除nodeName属性等于给定属性名name的节点并返回被删除属性的Attr节点 |
setNamedItem() | Attr节点 node | 向列表中添加node节点,以其nodeName为索引 |
item() | 索引位置pos | 返回索引位置pos处的节点 |
注意:
-
NamedNodeMap对象中是一系列Attr节点,这些节点是元素的属性转化而来,要获得其属性名则用nodeName属性,要获取其属性值则用nodeValue属性。同样可以使用nodeValue属性重新设置属性值。
let id = element.attributes.getNamedItem("id"); //element.attributes["id"]一样 //id.nodeValue就是其属性值 id.nodeValue="new id"; //重新设置
-
使用attributes属性与前面谈到的方法其实作用差不多,一个是将属性放在一个属性集合中再对属性进行操纵,而另一个则是直接在元素上进行操纵。对于开发者来说,更喜欢使用
getAttribute()
、setAttribute()
和removeAttribute()
方法。 -
attributes 属性最有用的场景是需要迭代元素上所有属性的时候。这时候往往是要把DOM结构序列化为XML或HTML字符串。如下:
function outputAttributes(element){ let pairs = []; for(let i=0;i<element.attributes.length;++i){ const attribute = element.attributes[i]; pairs.push(
${attribute.nodeName}="${attribute.nodeValue}"
); } return pairs.join(" "); }这个函数使用数组存储每个名/值对,迭代完所有属性后,再将这些名/值对使用空格拼接在一起(这个技术常常用于序列化为长字符串。)
创建元素
方法 | 参数 | 返回值 | 说明 |
---|---|---|---|
document.createElement() | 要创建元素的标签名(字符串形式) | 返回一个节点 | 在HTML文档中,标签名是不区分大小写的,而XML文档区分。 |
注意:
- 使用
createElement()
方法创建新元素的同时也会将其ownerDocument 属性设置为 document 。此时可以再为其添加属性等。 - 新元素需要使用
appendChild()
、insertBefore()
或replaceChild()
方法添加到文档中。
元素后代
元素可以拥有任意多个子元素和后代元素。childNodes属性包含元素所有的子节点,这些子节点可能是其他元素、文本节点、注释或者处理指令。所以当遍历元素子节点时,需要先判断子节点是何种类型再进行操作。
for(let i=0;i<element.childNodes.length;++i){
if(element.childNodes[i].nodeType==1){
//当为元素节点时进行操作
}
}
Text 类型
Text 节点由 Text 类型表示,包含按字面解释的纯文本,也可能包含转义后的HTML字符。但不包含HTML代码。
Text 类型的节点的parentNode值为Element对象,且不支持子节点。
Text 节点中包含的文本有两种方式进行访问,一种是通过nodeValue属性访问,另一种是通过data属性访问,两者相同。文本节点的操作方法有如下几个:
方法 | 参数 | 返回值 | 说明 |
---|---|---|---|
appendData(text) | 字符串text | 无 | 向节点末尾添加文本text |
deleteDate(offset, count) | 偏移位置offset、字符个数count | 无 | 从位置offset开始删除count个字符 |
insertData(offset, text) | 偏移位置offset、字符串text | 无 | 在位置offset插入文本text |
replaceData(offset, count, text) | 偏移位置offset、字符个数count、字符串text | 无 | 用文本text替换从位置offset到offset+count(不包含)的文本 |
splitText(offset) | 偏移位置offset | 返回位置从offset(包含)到末尾的新Text 节点 | 在位置offset 将当前文本节点拆分成两个文本节点。两个文本节点在同一个childNodes中 |
substringData(offset, count) | 偏移位置offset、字符个数count | 返回从位置offset到offset+count的字符串 | 提取从位置offset到offset+count的文本 |
除了以上方法外,还可以通过length属性获取文本节点中包含的字符数量。这个值等于 nodeValue.length 和 data.length。
默认情况下,包含文本内容的每个元素最多只能有一个文本节点(但可以增加文本节点),只要开始标签和结束标签之间有内容,就会创建一个文本节点。当取得文本节点的引用后可以使用 nodeValue 属性进行修改。但需要注意的是,HTML或XML代码会转换成实体编码(就是直接展示而不会被浏览器渲染),这实际上是在将HTML字符串插入DOM文档浅进行编码的有效方式。
div.firstChild.nodeValue="some <strong>other</strong> message"; //这样就会直接展示,而不是加粗
创建文本节点
方法 | 参数 | 返回值 | 说明 |
---|---|---|---|
document.createTextNode() | 字符串文本 | 文本节点 | 跟设置已有的文本节点的值一样,这些要插入的文本也会应用HTML或XML编码而不会渲染。 |
注意:
- 创建新文本节点后,其 ownerDocument 属性会被设置为 document。注意不是在元素上使用createTextNode()方法,而是在document上使用。
- 和创建新元素一样,需要将创建的文本节点添加到文档树中。
- 在将一个文本节点作为另一个文本节点的兄弟插入后,两个文本节点的文本之间不会包含空格。
- 若是第三点的情况,则会出现多个文本节点作为兄弟,这时为了规范化文本节点,可以使用normalize()方法合并文本节点(在文本节点的父节点上使用)
拆分文本节点
Text 类型定义了一个与 normalize() 相反的方法—— splitText() 。这个方法将一个文本节点拆分成两个文本节点。拆分后,原来的文本节点包含开头到偏移位置前的文本(不包含),新文本节点包含剩下的文本。返回新的文本节点,新的文本节点和原文本节点有相同的parentNode,即在新文本节点会直接插入到childNodes中。
拆分文本节点最常用于从文本节点中提取数据的DOM解析技术。
Comment 类型
- DOM中的注释通过Comment 类型表示。同样,它也不支持子节点。
- Comment 类型与Text 类型继承同一个基类(CharacterData),因此拥有除splitText()之外Text 节点所有的字符串操作方法。与Text 类型相似,注释的实际内容可以通过nodeValue或data属性获得。
- 注释节点可以作为父节点的子节点来访问。
- 可以使用document.createComment()方法创建注释节点。
- 浏览器不承认结束的
</html>
标签之后的注释。如果要访问注释节点,则必须确定它们是<html>
元素的后代。
CDATASection 类型
这类型是XML中特有的。表示XML中的CDATA区块。遇到后再自查。
DocumentType 类型
Document 类型的节点包含文档的文档类型(doctype)信息。具有以下特征:
- nodeType 等于10;
- nodeName 值为文档类型的名称;
- nodeValue 值为null;
- parentNode 值为Document 对象;
- 不支持子节点。
属性名 | 说明 |
---|---|
name | 文档类型的名称 |
entities | 文档类型描述的实体的NamedNodeMap |
notations | 文档类型描述的表示法的NamedNodeMap |
DocumentFragment 类型
在所有节点类型中,DocumentFragment 类型是唯一一个在标记中没有对应表示的类型。DOM将文档片段定义为 “轻量级” 文档,能够包含和操作节点,却没有完整文档那样的额外消耗。DocumentFragment 节点具有以下特征:
- nodeType 等于11;
- nodeName 值为“#document-fragment”;
- nodeValue 值为null;
- parentNode 值为null;
- 子节点可以是Element、ProcessingInstruction、Comment、Text、CDATASection或EntityReference。
文档片段的作用就是一个仓库,这个仓库中存储着要被添加到文档的节点。为什么这样做呢?因为若每创建一个节点就添加到文档中,则网页就会被重新渲染一次,为避免多次渲染,就可以先将需要添加的节点放入仓库中(这个仓库也是一个文档,有节点关系),再一次性添加到文档中(其实就是换了一个父节点)。这样就会只重新渲染一次。
文档片段虽然也是节点,但这个节点永远不会被添加到文档树中,使用appendChild()等方法只会将存入这里面的节点全部添加到文档树中。使用document.createDocumentFragment()
方法可以创建文档片段,返回一个节点对象。
let fragment = document.createDocumentFragment();
文档片段从Node 类型继承了所有文档类型具备的可以执行DOM操作的方法。如果文档中的一个节点添加到一个文档片段,则该节点从文档树中移除,不会被浏览器渲染。因为文档片段不在文档树中,因此文档片段中的节点同样不会被浏览器渲染。可以通过appendChild()或insertBefore()方法来回切换(文档树中的节点使用该方法添加文档片段则将文档片段中的节点添加到文档树中,相反,文档片段调用该方法则会将节点添加到文档片段中)。以下是在<ul>
元素中添加3个<li>
有元素,并只重新渲染一次。
let fragment = document.createDocumentFragment(); //创建文档片段节点
let ul = document.getElementById("myList"); //获取ul引用
for(let i=0;i<3;++i){
let li = document.createElement("li"); //创建li节点
li.appendChild(document.createTextNode(Item ${i+1}
)); //li节点添加文本
fragment.appendChild(li); //将li节点添加至文档片段中
}
ul.appendChild(fragment); //将文档片段中的3个li节点同时添加到ul的下面
Attr 类型
元素数据在DOM中通过 Attr 类型表示。属性是存在于元素 attributes 属性中的节点。Attr 节点具有以下特征:
- nodeType 等于2;
- nodeName 值为属性名;
- nodeValue 值为属性值;
- parentNode 值为null;
- 在HTML中不支持子节点;
- 在XML中子节点可以是Text或EntityReference。
属性节点尽管是节点,却不认为是DOM文档树中的一部分。
属性名 | 说明 |
---|---|
name | 属性名(与nodeName一样) |
value | 属性值(与nodeValue一样) |
specified | 值为布尔值,表示属性使用的是默认值还是被指定的值 |
方法 | 参数 | 返回值 | 说明 |
---|---|---|---|
document.createAttribute() | 属性名(字符串形式) | 返回一个属性节点对象 | 创建一个属性节点,然后需要通过value属性(或nodeValue属性)设置属性值,再使用下方方法添加到元素的中 |
element.setAttributeNode() | 属性节点对象 | 无 | 与上面方法一起用 |
注意:将属性作为节点来访问多数情况下并无必要。推荐使用 getAttribute()
、removeAttribute()
和setAttribute()
方法操作属性,而不是直接操作属性节点。