HTML中的DOM对象

DOM

DOM(文档对象模型)是针对 HTML 和 XML 文档的一个API。

  • 主流浏览器都完成了 DOM1 级的实现
  • IE 的 DOM 对象都是以 COM 对象的形式存在,和原始的 DOM 对象行为并不一致

一、节点层次

DOM 可以将 HTML 或 XML 文档描绘成一个由多层节点构成的结构,简单点说,就是一个树结构。

1.1 文档节点

文档节点是每个文档的根节点。

文档节点不直接表现在文档定义中,是一个内置的节点。

1.2 文档元素

文档元素是文档的最外层元素,文档中的所有其他元素都包含在文档元素中。

每个文档只能有一个文档元素。

在 HTML 页面中,文档元素始终是 <html> 元素。

1.3 节点结构

以 HTML 文档为例,结构如下:

1
2
3
4
5
6
7
|-- Document
|-- html
|-- head
|-- title
|-- body
|-- p
|-- div

其中,Document 是文档节点,它在 HTML 文档中是没有定义的;html 是文档元素,所有其他的元素都在它里面。

二、节点类型

2.1 Node 类型

Node 在 DOM1级定义中,是一个接口,由 DOM 中的所有节点类型实现。

在 JavaScript 中,Node 接口是作为 Node 类型实现的。

  • JavaScript 中的所有节点类型都继承自 Node 类型
  • 每个节点都有一个 nodeType 属性,用于表明节点的类型
  • 除 IE 外,其他浏览器均可以访问 Node 类型(IE 没有公开它的 Node 类型的构造函数)

2.1.1 公共属性

(1) nodeType

每个节点都有一个 nodeType 属性,用于表明节点的类型。节点类型总共有12种(括号内的是它的常量值):

1
2
3
4
5
6
7
8
9
10
11
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)

需要说明的是,并不是所有 Web 浏览器都支持全部的节点类型。

由于 IE 无法访问 Node 类型,因此在判断节点类型时,为了确保浏览器兼容性,最好将 nodeType 与数值作比较:

1
2
3
4
5
6
7
8
9
// IE 下无效
if (node.nodeType === Node.ELEMENT_NODE) {
...
}

// 适用于所有浏览器
if (node.nodeType === 1) {
...
}

(2) nodeName 和 nodeValue

这两个值取决于节点的类型,有可能取值为 null。

Node.ELEMENT_NODE 元素类型中,nodeName 始终保存的是元素的标签名,而 nodeValue 的值则始终是 null。

(3) childNodes

每个节点都有一个 childNodes 属性,用于保存它的子节点。

childNodes 是一个 NodeList 对象,是一种类数组对象(不是数组),其中保存着一组有序的节点。

NodeList 对象是一个动态查询的结果,也就是说,当 DOM 结构发生变化时,它能够实时更新数据。

1
2
3
4
5
6
7
8
// 获取 NodeList 对象的长度
var length = node.childNodes.length

// 使用类似数组的[]下标访问
var firstChild = node.childNodes[0]

// 使用 item() 方法访问
var lastChild = node.childNodes.item(length - 1)

需要说明的是,虽然所有节点都继承自 Node 类型,但并不是每种节点都有子节点。

(4) 其他属性

  • parentNode:父节点
  • previousSibling:同级节点的前一个节点
  • nextSibling:同级节点的下一个节点
  • firstChild:第一个子节点
  • lastChild:最后一个子节点
  • ownerDocument:节点所在的文档

2.1.2 公共方法

(1) appendChild

appendChild() 方法用于向 childNodes 列表末尾追加一个节点。

如果追加的节点已经在 childNodes 中,则会将该节点移动到 childNodes 列表的末尾。

1
2
3
4
5
6
7
8
// 追加到末尾
var newNode = parentNode.appendChild(newChildNode)
alert(newNode === parentNode.lastChild) // true

// 移动到末尾
var firstChild = parentNode.firstChild
var moveNode = parentNode.appendChild(firstChild)
alert(moveNode === parentNode.lastChild) // true

(2) insertBefore

insertBefore() 方法用于在 childNodes 列表的某个位置中插入一个节点。

1
2
3
4
5
6
7
// 插入到末尾
var newNode = parentNode.insertBefore(newChildNode, null)
alert(newNode === parentNode.lastChild) // true

// 插入后成为第一个节点
var insertNode = parentNode.insertBefore(newChildNode, parentNode.firstChild)
alert(insertNode === parentNode.firstChild) // true

(3) replaceChild

replaceChild() 方法用于将旧节点替换成新节点,实际上是做了一个删除节点操作和插入节点操作。

1
2
3
// 替换指定的子节点
var replaceNode = parentNode.replaceChild(newChildNode, parentNode.firstChild)
alert(replaceNode === parentNode.firstChild)

(4) removeChild

removeChild() 方法用于移除子节点。

1
2
// 移除第一个子节点
var removeNode = parentNode.removeChild(parentNode.firstChild)

(5) 其他方法

  • cloneNode:用于创建调用节点的副本,cloneNode(true) 为深复制,cloneNode(false) 为浅复制。
  • normalize:用于处理后代的文本节点,后代中如果存在空的文本节点,则删除它;如果出现相邻的文本节点,则合并它们。

2.2 Document 类型

Document 类型用于表示文档,可以是 HTML 页面或者基于 XML 的文档。

  • 在浏览器中,document 对象是 HTMLDocument(继承自 Document 类型)的一个实例,表示整个 HTML 页面
  • document 对象是 window 对象的一个属性,可作全局对象访问

具有的特征包括:

  • nodeName 的值为 #document
  • nodeValue 的值为 null
  • parentNode 的值为 null
  • ownerDocument 的值为 null

(1) 文档子节点

DOM 标准规定 Documnet 节点的子节点可以是:

  • DocumentType(最多一个)
  • Element(最多一个)
  • ProcessingInstruction
  • Comment

document 对象拥有两个内置的访问其子节点的快捷方式,一个是 documentElement,另一个是 body

  • document.documentElement 可以直接访问文档的 <html> 元素(因为文档最多只会有一个 Element 元素)
  • 作为 HTMLDocument 的实例,document.body 还可以直接访问文档的 <body> 元素

其他子节点属性:

  • DocumentType:document.doctype 用来表示文档类型子节点,但是由于浏览器对 document.doctype 的支持不一致,因此这个属性的用处不大
  • Comment:理论上出现在 <html> 外面的注释,都属于 document 的子节点,但是不同浏览器对于这种注释的处理也存在差异,因此文档注释的意义也不大

(2) 文档信息

  • title:document.title 表示文档标题元素 <title> ,修改 document.title 的值会直接改变 <title> 元素。
  • URL:document.URL 表示当前页面的完整 URL(即地址栏显示的 URL)
  • domain:document.domain 表示页面的域名,与 URL 相关联
  • referer:document.refreer 表示来源页面的 URL(即跳转到当前页面的上一个页面 URL)
1
2
3
4
5
6
7
8
9
10
11
// 修改页面标题
document.title = '新标题'

// http://www.baidu.com/
var url = document.URL

// www.baidu.com
var domain = document.domain

// http://www.google.com/
var referer = document.refreer

在 URL、domain、referer 中,只有 domain 可以修改,其他两个都是只读属性。

  • domain 只能设置成 URL 的子域名,不能修改为 URL 中不包含的域
  • 修改为子域名之后,不能再修改回原域名(即修改只能收缩,不能扩张)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// http://www.baidu.com/
var url = document.URL

// www.baidu.com
var domain = document.domain

// 可以从父域名修改为子域名
document.domain = 'baidu.com'

// 不可以修改为 URL 不包含的域名
document.domain = 'google.com' // 出错

// 修改为子域名以后,不可以再修改回原域名
document.domain = 'www.baidu.com' // 出错

domain 的限制来源于跨域安全:来自不同子域的页面不能通过 Javascript 通信。也就是说,如果一个页面包含了一个内嵌框架,而且内嵌框架的域名和页面的域名不同,那它们之间就不能互相访问对方的 Javascript 对象。

1
2
3
4
5
6
7
8
9
10
11
12
<!-- 假设当前页面来自 http://wwww.baidu.com/outer.html -->
<html>
<body>
<frameset>
<frame src="http://www.google.com/inner.html", name="innerFrame">
</frameset>
<script>
// 由于页面和内置框架不同域名,因此会受到跨域安全的限制
// 导致在这里无法正常访问内嵌框架的 Javascript 对象
</script>
</body>
</html>

想要通过 Javascript 实现跨域访问,可以使用 window.postMessage() 方法。

(3) 特征检测

由于 DOM 分为多个级别,也包含多个部分,因此在使用某些特征功能之前,需要检测一下浏览器是否实现来该功能。

document.implemetation 属性提供了浏览器实现的相应信息和功能,其中 DOM1 级规定了一个方法 hasFeature() 用于检测 DOM 功能以及版本:

1
var hasXmlDom = document.implementation.hasFeature('XML', '1.0')

需要注意的是,hasFeature() 方法判断可能存在错误,因为功能是由浏览器负责实现的,但它未必完全按照 DOM 规范来实现,因此即使是在 hasFeature() 返回 true情况下,也未必能正确使用该功能,所以除了使用 hasFeature() 以外,应该还要加上能力检测。

(4) 查找元素

Document 类型查找元素的方法有,搜索的范围是整个文档:

  • getElementById:通过元素的 ID 获取,区分大小写。如果文档中有多个相同 ID 的元素,则返回第一次出现的元素
  • getElementsByTagName:通过元素的标签名获取,返回的是包含0个或多个元素的 NodeList
1
2
3
4
5
6
7
8
// 按照 ID 获取
var myDiv = document.getElementById('divId')

// 按照标签获取
var images = document.getElementsByTagName('img')

// 获取所有元素
var allElements = document.getElementsByTagName('*')

另外,对于 HTMLDocument 类型而言,还有另外的方法:

  • getElementsByName:通过元素的name特性获取,返回的是包含0个或多个元素的 NodeList

getElementsByName() 方法常用在获取同一组单选按钮的值:

1
2
// 按照name属性获取
var radios = document.getElementsByName('radioName')

另外,文档为常用的元素提供了快捷方式,可以免去我们自己查找:

  • document.anchors:文档中所有带有 name 特性的 <a> 元素
  • document.links:文档中所有带有 href 特性的 <a> 元素
  • document.applets:文档中所有的 <applet> 元素(不建议使用)
  • document.forms:文档中所有的 <form> 元素
  • document.images:文档中所有的 <img> 元素

(5) 文档写入

document 对象可以直接将输出流写入网页中,主要的方法有几个:

  • write:写入字符串
  • writeln:写入字符串,并在末尾添加一个换行符(\n
  • open:打开输出流
  • close:关闭输出流

需要注意的是,在文档加载期间使用 write()writeln() 是不需要使用 open()close() 方法的。

1
2
3
4
5
6
7
8
<html>
<body>
<script>
// 处于文档加载期间,不需要使用 open() 和 close() 方法
document.write('Hello world!');
</script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
<html>
<body>
<button onclick="btnClick()"></button>
<script>
function btnClick () {
// 文档加载完毕,需要使用 open() 和 close() 方法
document.open();
document.write('Hello world!');
document.close();
}
</script>
</body>
</html>

2.3 Element 类型

元素 Element 类型用于表示 XML 或 HTML 元素,具有的特征包括:

  • nodeType 的值为 1
  • nodeName 的值为元素的标签名
  • nodeValue 的值为 null
  • parentNode 可能是 Document 或 Element

访问元素的标签名可以使用 nodeName 或 tagName,在 Element 类型中这两个是相等的。

  • 在 HTMl 中,标签名始终是全部大写表示
  • 在 XML 中,标签名则始终和源代码中保持一致

因此在判断标签名时,最好转换一下大小写:

1
2
3
if (element.tagName.toUpperCase() === 'DIV') {
...
}

(1) HTML 元素

所有 HTML 元素都由 HTMLElement 类型表示,而 HTMLElement 继承自 Element,并添加了一些属性:

  • id:元素的唯一标识符
  • title:元素的附加说明信息,一般用于工具提示条显示
  • lang:元素内容的语言代码,少用
  • dir:语言的方向,少用
  • className:元素的 class 特征,即元素的 CSS 类(class 是 ECMAScript 保留字,所以用 className 表示)

(2) 元素特性

特性是用于给出元素及其内容的附加信息。

1
2
<!-- id、class、title、align、data-custom-attr 等称为元素特性 -->
<div id="myDiv" class="my-cls" title="my-title" align="left" data-custom-attr="custom attribution"></div>

操作特性的 DOM 方法有几个:

  • getAttribute:获取特性
  • setAttribute:设置特性
  • removeAttribute:删除特性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var myDiv = document.getElementById('myDiv')

// 设置特性
myDiv.setAttribute('id', 'new_id')
myDiv.setAttribute('class', 'new-my-cls')
myDiv.setAttribute('title', 'New title')

// 获取特性
alert(myDiv.getAttribute('id')) // "new_id"
alert(myDiv.getAttribute('class')) // "new-my-cls"
alert(myDiv.getAttribute('title')) // "New title"

// 移除特性
myDiv.removeAttribute('class')

(3) 元素属性

通过元素属性还可以访问元素特性,元素特性与元素属性的关系如下:

  • 元素的特性可以通过 DOM 元素对象的属性访问
  • 只有公认的(非自定义的)特性才会以属性的形式添加到 DOM 对象中(如 id、title 等)
  • 通过属性获取得到的特性是经过解析的,是一个对象
1
2
3
4
5
6
7
8
9
// element.id、element.className、element.title 等称为元素属性
var myDiv = document.getElementById('myDiv')
alert(myDiv.id) // "myDiv"
alert(myDiv.className) // "my-cls"(class 是保留字的原因)
alert(myDiv.title) // "my-title"

// 自定义特性不会以属性的形式直接显示
alert(myDiv['data-custom-attr']) // undefined(IE除外)
alert(myDiv.getAttribute('data-custom-attr')) // "custom attribution"

有两类特性,它们虽然有对应属性名,但是属性值和通过 getAttribute() 拿到的特性值不同,属性值是经过解析的对象,而特性值是未经解析的字符串。这两类特性分别是样式 style,事件处理程序(onclick 等)。

1
<div id="myDiv" style="height:100px;" onclick="btnClick"></div>
1
2
3
4
5
6
7
8
9
var myDiv = document.getElementById('myDiv')

// 会直接得到原始的字符串
alert(myDiv.getAttribute('style')) // "height:100px;"
alert(myDiv.getAttribute('onclick')) // "btnClick"

// 会得到解析过的对象/方法
alert(myDiv.style) // object
alert(myDiv.onclick) // function

另外,Element 类型还有一个属性 attributes 用于保存元素所有的元素特性。element.attributes 属性包含一个 NamedNodeMap,是一个“动态”的集合,能够动态实时获取元素的所有特征。

(4) 创建元素

使用 document.createElement() 可以创建新元素,创建的同时,也为新元素设置了 ownerDocument 属性。

1
2
3
4
var newDiv = document.createElement('div')

// 新元素需要添加到文档树中,才会在页面上显示
document.body.appendChild(newDiv)

(5) 查找元素

元素支持 getElementsByTagName() 方法,搜索起点为当前元素,范围是当前元素的子元素。

1
2
3
// 查找指定元素下面的图片
var myDiv = document.getElementById('myDiv')
var images = myDiv.getElementsByTagName('img')

2.4 Text 类型

文本节点由 Text 类型表示,包含的是纯文本内容,不能包含 HTML 代码,具有的特征:

  • nodeType 的值为 3
  • nodeName 的值为 “#text”
  • nodeValue 的值为节点所包含的文本
  • parentNode 是一个 Element
  • 不支持(没有)子节点

文本节点的文本可以通过 nodeValue 或 data 属性获取,它们是相同的。

  • 默认情况下,每个可以包含内容的元素最多只能有一个文本节点,而且必须确实有内容存在
  • 修改文本时,字符串会经过 HTML (或 XML)编码(例如大于号、小于号等被转义成其他字符串)
  • 如果两个文本节点是相邻的同胞节点,那它们的文本就会连起来显示,并且不会存在任何空格
1
2
// 显示结果是 "&lt;p&gt;New text content&lt;/p&gt;"
textNode.nodeValue = "<p>New text content</p>"

(1) 创建文本节点

创建文本节点使用 document.createTextNode() 方法,方法接收文本作为参数,创建节点的同时,也会设置节点的 ownerDocument 属性。

1
var newTextNode = document.createTextNode('Hello world!')

需要注意的是,和设值一样,创建时的参数文本也会经过 HTML (或 XML)编码。

默认情况下,元素只有一个文本节点,但是通过 Javascript 可以为元素创建多个文本子节点:

1
2
3
4
5
6
7
var myDiv = document.getElementById('myDiv')

var newTextNode1 = document.createTextNode('Hello')
var newTextNode2 = document.createTextNode(' world!')

myDiv.appendChild(newTextNode1)
myDiv.appendChild(newTextNode2)

(2) 规范化文本

前面说过,如果两个节点是相邻的同胞节点,它们的文本会连起来显示,但是这样会导致分不清哪个字符串是哪个文本节点的,因此为了合并相邻的同胞文本节点,DOM 提供了一个方法 normalize(),用于合并相邻的文本节点。

1
2
3
4
5
6
7
8
9
10
var myDiv = document.getElementById('myDiv')

var newTextNode1 = document.createTextNode('Hello')
var newTextNode2 = document.createTextNode(' world!')

myDiv.appendChild(newTextNode1)
myDiv.appendChild(newTextNode2)

// 合并同胞文本节点
myDiv.normalize()

(3) 分割文本

Text类型除了可以合并以外,还可以进行分割:splitText()。这个方法会将1个文本节点分割成2个:

1
2
3
4
5
6
7
8
9
var myDiv = document.getElementById('myDiv')

var newTextNode = document.createTextNode('Hello world!')
myDiv.appendChild(newTextNode)

var splitNode = myDiv.firstChild.splitText(5)
alert(myDiv.firstChild.nodeValue) // "Hello"
alert(splitNode.nodeValue) // " world!"
alert(myDiv.childNodes.length) // 2

2.5 Comment 类型

Comment 类型用于表示注释,与 Text 类型类似,具有特征如下:

  • nodeType 的值为 8
  • nodeName 的值为 “#comment”
  • nodeValue 的值是注释的内容
  • 不支持(没有)子节点

Comment 类型和 Text 类型继承自相同的基类,因此拥有除了 splitText() 方法以外的所有字符串操作方法。

创建注释节点可以通过使用 document.createComment(),参数是注释内容:

1
var comment = document.createComment('new comment')

2.6 CDATASection 类型

CDATASection 类型只针对基于 XML 的文档,表示的是 CDATA 区域。具有的特征如下:

  • nodeType 的值为4
  • nodeName 的值为 “#cdata-section”
  • nodeValue 的值是 CDATA 区域的内容
  • 不支持(没有)子节点

与 Comment 类型类似,CDATASection 类型继承自 Text 类型,因此拥有除了 splitText() 方法以外的所有字符串操作方法。

CDATA 区域只会出现在 XML 文档中,因此浏览器一般会把 CDATA 区域错误解析为 Comment 或 Element。

在 XML 文档中创建 CDATA 区域,可以使用 document.createCDATASection() 方法,参数是区域的内容。

2.7 DocumentType 类型

DocumentType 类型包含着与文档的 doctype 有关的所有信息。具有的特征包括:

  • nodeType 的值为 10
  • nodeName 的值为 doctype 的名称
  • nodeValue 的值为 null
  • parentNode 为 Document
  • 不支持(没有)子节点

在 DOM1 级中,DocumentType 对象不能动态创建,只能通过文档解析的方式创建。支持 DocumentType 的浏览器会将生成的 DoocumentType 对象保存在 document.doctype 中。

2.8 DocumentFragment 类型

DocumentFragment 类型表示文档片段,是一种“轻量级”的文档,可以包含和控制部分节点,但是不像完整文档那样占用额外的资源。在所有的节点类型中,只有 DocumentFragment 类型在文档中没有对应的标记。具有的特征包括:

  • nodeType 的值为 11
  • nodeName 的值为 “#document-fragment”
  • nodeValue 的值为 null
  • parentNode 的值为 null

文档片段不能直接添加到文档中,但是可以把文档片段中的节点添加到文档中。因此文档片段相当于一个节点临时仓库,可以将临时生成的节点保存在文档片段中,最后再将生成的节点都添加到文档中。

创建文档片段可以使用 document.createDocumentFragment() 方法:

1
var fragment = document.createDocumentFragment()
  • 如果将文档中的节点添加到文档片段中,该节点就会从文档树中移除(也就是浏览器看不到了)
  • 将文档片段的子节点添加到文档中,节点会从文档片段中移除,但是文档片段不会添加到文档树上
  • 文档片段有类似于缓存的作用,可以用于避免浏览器反复渲染新信息
1
2
3
4
5
6
7
8
9
10
11
var ul document.createElement('ul')

// 可以暂时保存到文档片段中,避免浏览器多次渲染新节点
var li = null
var fragment = document.createDocumentFragment()
for (var i = 0; i < 5; i++) {
li = document.createElement('li')
fragment.appendChild(li)
}

ul.appendChild(fragment)

2.9 Attr 类型

元素的特性在 DOM 中以 Attr 类型来表示。在所有浏览器中,都可以访问 Attr 类型的构造器和原型。从技术角度讲,特性就是存在于元素的 attributes 属性中的节点。具有的特征包括:

  • ndoeType 的值为 2
  • nodeName 的值就是特性的名称
  • nodeValue 的值是特性的值
  • parentNode 的值为 null
  • 在 HTML 中不支持(没有)子节点
  • 在 XML 中可以有 Text 或者 EntityReference 子节点

创建 Attr 节点可以使用 document.createAttribute() 方法,Attr 节点创建完成后,还需要使用 setAttribute() 方法来将属性值设到元素中:

1
2
3
4
5
6
var myDiv = document.getElementById('myDiv')

var attr = document.createAttribute('align')
attr.value = 'left'

myDiv.setAttribute(attr)

但是在开发中,直接使用 setAttribute() 方法会更加直接一些:

1
2
var myDiv = document.getElementById('myDiv')
myDiv.setAttribute('align', 'left')
作者

jiaduo

发布于

2021-08-28

更新于

2023-04-02

许可协议