DOM

DOM 表示 由多层节点构成的文档,通过它开发者可以添加、删除和修改页面的各个部分。

DOM 现在是真正跨平台、语言无关的表示和操作网页的方式。

节点层级

任何 HTML 或 XML 文档都可以用 DOM 表示为一个由节点构成的层级结构

document 节点表示每个文档的根节点

根节点的唯一子节点是<html>元素,我们称之为文档元素

文档元素是文档最外层的元素,所有其他元素都存在于这个元素之内。每个文档只能有一个文档元素。

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

在 XML 文档中, 则没有这样预定义的元素,任何元素都可能成为文档元素。

每个节点都有 nodeType 属性,表示该节点的类型。节点类型由定义在 Node 类型上的 12 个数值常量表示, 浏览器并不支持所有节点类型

nodeName 与 nodeValue

nodeNamenodeValue 保存着有关节点的信息。

节点关系

文档中的所有节点都与其他节点有关系。这些关系可以形容为家族关系,相当于把文档树比作家谱。 在 HTML 中,<body>元素是<html>元素的子元素,而<html>元素则是<body>元素的父元素。 <head>元素是<body>元素的同胞元素,因为它们有共同的父元素<html>

每个节点都有一个 childNodes 属性,其中包含一个 NodeList 的实例。NodeList 是一个类数组对象,用于存储可以按位置存取的有序节点。

NodeList 对象独特的地方在于,它其实是一个对 DOM 结 构的查询,因此 DOM 结构的变化会自动地在 NodeList 中反映出来。我们通常说 NodeList 是实时的 活动对象,而不是第一次访问时所获得内容的快照。

使用中括号或使用 item()方法访问 NodeList 中的元素:

1
2
3
let firstChild = someNode.childNodes[0]; 
let secondChild = someNode.childNodes.item(1);
let count = someNode.childNodes.length;

每个节点都有一个 parentNode 属性,指向其 DOM 树中的父元素。childNodes 中的所有节点都 有同一个父元素,因此它们的 parentNode 属性都指向同一个节点。此外,childNodes 列表中的每个 节点都是同一列表中其他节点的同胞节点。而使用 previousSibling 和 nextSibling 可以在这个列 表的节点间导航。这个列表中第一个节点的 previousSibling 属性是 null,最后一个节点的 nextSibling 属性也是 null。

注意,如果 childNodes 中只有一个节点,则它的 previousSibling 和 nextSibling 属性都是 null

有了这些关系,childNodes 属性的作用远远不止是必备属性那么简单了。这是因为利用这些关系指针,几乎可以访问到文档树中的任何节点

hasChildNodes(),这个方法如果返回 true 则说明节点有一个或多个子节点。相比查询 childNodes 的 length 属性,这个方法无疑更方便

ownerDocument 属性是一个指向代表整个文档的文档节点的指针。这个属性为迅速访问文档节点提供了便利,因为无需在文档结构中逐层上溯了。

操纵节点

DOM提供了一些操纵节点的方法

1.appendChild(),用于在 childNodes 列表末尾添加节点

添加新节点会更新相关的关系指针,包括 父节点和之前的最后一个子节点。appendChild()方法返回新添加的节点

如果把文档中已经存在的节点传给 appendChild(),则这个节点会从之前的位置被转移到新位置。

2.如果想把节点放到 childNodes 中的特定位置而不是末尾,则可以使用 insertBefore()方法。 这个方法接收两个参数:要插入的节点和参照节点

调用这个方法后,要插入的节点会变成参照节点的 前一个同胞节点,并被返回。

1
2
3
4
5
6
7
8
9
10
// 作为最后一个子节点插入
returnedNode = someNode.insertBefore(newNode, null);
alert(newNode == someNode.lastChild); // true
// 作为新的第一个子节点插入
returnedNode = someNode.insertBefore(newNode, someNode.firstChild);
alert(returnedNode == newNode); // true
alert(newNode == someNode.firstChild); // true
// 插入最后一个子节点前面
returnedNode = someNode.insertBefore(newNode, someNode.lastChild);
alert(newNode == someNode.childNodes[someNode.childNodes.length - 2]); // true

注意:appendChild() 和 insertBefore() 在插入节点时不会删除任何已有节点。

3.replaceChild()方法接收两个参数:要插入的节点和要替换的节点。要替换的节点会被返回并从文档 树中完全移除,要插入的节点会取而代之。

1
2
3
4
// 替换第一个子节点
let returnedNode = someNode.replaceChild(newNode, someNode.firstChild);
// 替换最后一个子节点
returnedNode = someNode.replaceChild(newNode, someNode.lastChild);

使用 replaceChild()插入一个节点后,所有关系指针都会从被替换的节点复制过来

4.要移除节点而不是替换节点,可以使用 removeChild()方法。这个方法接收一个参数,即要移除 的节点。被移除的节点会被返回

1
2
3
4
// 删除第一个子节点
let formerFirstChild = someNode.removeChild(someNode.firstChild);
// 删除最后一个子节点
let formerLastChild = someNode.removeChild(someNode.lastChild);

这4 个方法都用于操纵某个节点的子元素,也就是说使用它们之前必须先取得父节点(使 用前面介绍的 parentNode 属性。并非所有节点类型都有子节点,如果在不支持子节点的节点上调用这些方法,则会导致抛出错误。

cloneNode(),会返回与调用它的节点一模一样的节点。

cloneNode()方法接收一个布尔值参数,表示是否深复制。

在传入 true 参数时,会进行深复制,即复制节点及其整个子DOM树。

如果传入false,则只会复制调用该方法的节点。

复制返回的节点属于文档所有,但尚未指定父节点,所以可称为孤儿节点。

normalize()。这个方法唯一的任务就是处理文档子树中的文本节点。

在节点上调用 normalize()方法会检测这个节点的所有后代,从中搜索上述两种 情形。如果发现空文本节点,则将其删除;如果两个同胞节点是相邻的,则将其合并为一个文本节点。

Document 类型

文档子节点

第一个是 documentElement 属 性,始终指向 HTML 页面中的<html>元素。

虽然 document.childNodes 中始终有<html>元素,但使用 documentElement 属性可以更快更直接地访问该元素。

document 对象还有一个 body 属性,直接指向<body>元素。因为 这个元素是开发者使用最多的元素,所以 JavaScript 代码中经常可以看到 document.body

文档信息

document 作为 HTMLDocument 的实例,还有一些标准 Document 对象上所没有的属性。这些属性 提供浏览器所加载网页的信息。其中第一个属性是 title,包含<title>元素中的文本,通常显示在浏 览器窗口或标签页的标题栏。通过这个属性可以读写页面的标题,修改后的标题也会反映在浏览器标题 栏上。不过,修改 title 属性并不会改变<title>元素。

URL、domain 和 referrer

URL 包含当前页面的完整 URL(地址栏中的 URL),domain 包含页面的域名,而 referrer 包含链接到当前页面的那个页面的 URL。如果当前页面没有来源,则 referrer 属性包含空字符串。

所有这些信息都可以在请求的 HTTP 头部信息中获取,只是在 JavaScript 中通过这几个属性暴露出来而已。

定位元素

使用 DOM 最常见的情形可能就是获取某个或某组元素的引用,然后对它们执行某些操作。

document 对象上暴露了一些方法,可以实现这些操作。getElementById()和 getElementsByTagName()就是 Document 类型提供的两个方法。

getElementById()方法接收一个参数,即要获取元素的 ID,如果找到了则返回这个元素,如果 没找到则返回 null。参数 ID 必须跟元素在页面中的 id 属性值完全匹配,包括大小写。

如果页面中存在多个具有相同 ID 的元素,则 getElementById()返回在文档中出现的第一个元素。

getElementsByTagName()是另一个常用来获取元素引用的方法。这个方法接收一个参数,即要获取元素的标签名,返回包含零个或多个元素的 NodeList。

在 HTML 文档中,这个方法返回一个 HTMLCollection 对象。

下面的代码会取得页面中所有的<img>元素并返回包含它们的 HTMLCollection:

1
let images = document.getElementsByTagName("img");

与 NodeList 对象一样,也可以使用中括号或 item()方法从 HTMLCollection 取得特定的元素。

HTMLCollection 对象还有一个额外的方法 namedItem(),可通过标签的 name 属性取得某一项 的引用。

1
2
<img src="myimage.gif" name="myImage"> 
let myImage = images.namedItem("myImage");

要取得文档中的所有元素,可以给 getElementsByTagName()传入*。

注意:对于 document.getElementsByTagName()方法,虽然规范要求区分标签的大小 写,但为了最大限度兼容原有 HTML 页面,实际上是不区分大小写的。如果是在 XML 页 面(如 XHTML)中使用,那么 document.getElementsByTagName()就是区分大小写的

第三个方法是 getElementsByName()。顾名思义,这个方法会返回具有给定 name 属性的所有元素。

getElementsByName()方法最常用于单选按钮,因为同 一字段的单选按钮必须具有相同的 name 属性才能确保把正确的值发送给服务器

特殊集合

document对象上还暴露了几个特殊集合,这些集合也都是 HTMLCollection 的实例。这些集合是访问文档中公共部分的快捷方式

DOM 兼容性检测

document.implementation 属性是一个对象,其中提供了与浏览器DOM 实现相关的信息和能力。 DOM Level 1 在 document.implementation 上只定义了一个方法,即 hasFeature()。这个方法接收两个参数:特性名称和 DOM 版本。如果浏览器支持指定的特性和版本,则 hasFeature()方法返回 true.

由于实现不一致,因此 hasFeature()的返回值并不可靠。目前这个方法已经被废弃,不再建议使 用。为了向后兼容,目前主流浏览器仍然支持这个方法,但无论检测什么都一律返回 true.

文档写入

向网页输出流中写入内容。这个能力对应 4 个方法:write()、 writeln()、open()和 close()

write()和 writeln()方法都接收一个字符串参数,可以将这个字符串写入网页中。

write()简单地写入文本,而 writeln()还会在字符串末尾追加一个换行符 (\n)

open()和 close()方法分别用于打开和关闭网页输出流。在调用 write()和 writeln()时,这两 个方法都不是必需的。

Element 类型

Element 表示XML或HTML 元素,对外暴露出访问元素标签名、子节点和属性的能力。

Element 类型的节点具有以下特征:

 nodeType 等于 1;

 nodeName 值为元素的标签名;

 nodeValue 值为 null;

 parentNode 值为 Document 或 Element 对象;

 子节点可以是 Element、Text、Comment、ProcessingInstruction、CDATASection、 EntityReference 类型。

可以通过 nodeName 或 tagName 属性来获取元素的标签名。这两个属性返回同样的值

div.tagName 返回的是”DIV”而不是 “div”。在 HTML 中,元素标签名始终以全大写表示;

在 XML(包括 XHTML)中,标签名始终与源代码中的大小写一致。如果不确定脚本是在 HTML 文档还是 XML 文档中运行,最好将标签名转换为小写形式,以便于比较:

1
2
3
4
5
6
if (element.tagName == "div"){ // 不要这样做,可能出错!
// do something here
}
if (element.tagName.toLowerCase() == "div"){ // 推荐,适用于所有文档
// 做点什么
}

第一个是容易出错的写法,因为 HTML文档中 tagName 返回大写形式的标签名。第二个先把标签名转换为全部小写后再比较,这是推荐的做法,因为这对 HTML 和 XML 都适用。

取得属性

与属性相关的 DOM 方法 主要有 3 个:

getAttribute()、setAttribute()和 removeAttribute()。

这些方法主要用于操纵属性,包括在 HTMLElement 类型上定义的属性。

getAttribute()方法也能取得自定义属性的值。自定义属性名应该前缀 data-以方便验证。

1
2
<div id="myDiv" my_special_attribute="hello!"></div> 
let value = div.getAttribute("my_special_attribute");

属性名不区分大小写,因此”ID”和”id”被认为是同一个属性。

通过 DOM 对象访问的属性中有两个返回的值跟使用 getAttribute()取得的值不一样。

1.首先是 style 属性,这个属性用于为元素设定 CSS 样式。

2.第二个属性其实是一类,即事件处理程序(或者事件属性),比如 onclick。

设置属性

与 getAttribute()配套的方法是 setAttribute(),这个方法接收两个参数:要设置的属性名 和属性的值。

如果属性已经存在,则 setAttribute()会以指定的值替换原来的值;如果属性不存在, 则 setAttribute()会以指定的值创建该属性。

注意,在 DOM 对象上添加自定义属性,如下面的例子所示,不会自动让它变成元素的属性

1
2
div.mycolor = "red"; 
alert(div.getAttribute("mycolor")); // null

删除属性

removeAttribute()用于从元素中删除属性。这样不单单是清除属性的值,而是会把整个属性完全从元素中去掉

attributes 属性

Element 类型是唯一使用 attributes 属性的 DOM 节点类型。attributes 属性包含一个 NamedNodeMap 实例,是一个类似 NodeList 的“实时”集合。

创建元素

document.createElement()方法创建新元素。这个方法接收一个参数,即要创建元素的标签名。

在 HTML 文档中,标签名是不区分大小写的,而 XML 文档(包括 XHTML)是区分大小写 的。

使用 createElement()方法创建新元素的同时也会将其 ownerDocument 属性设置为 document。 此时,可以再为其添加属性、添加更多子元素。

在新元素上设置这些属性只会附加信息。因为这个元素还没有添加到文档树,所以不会影响浏览器 显示。要把元素添加到文档树,可以使用 appendChild()、insertBefore()或 replaceChild()。

元素被添加到文档树之后,浏览器会立即将其渲染出来。之后再对这个元素所做的任何修改,都会立即在浏览器中反映出来。

元素后代

元素可以拥有任意多个子元素和后代元素,因为元素本身也可以是其他元素的子元素。childNodes 属性包含元素所有的子节点,这些子节点可能是其他元素、文本节点、注释或处理指令。不同浏览器在 识别这些节点时的表现有明显不同。

1
2
3
4
5
<ul id="myList"> 
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>

在解析以上代码时,<ul>元素会包含 7 个子元素,其中 3 个是<li>元素,还有 4 个 Text 节点(表示<li>元素周围的空格)。

要取得某个元素的子节点和其他后代节点,可以使用元素的 getElementsByTagName()方法。在 元素上调用这个方法与在文档上调用是一样的,只不过搜索范围限制在当前元素之内,即只会返回当前元素的后代。

Text 类型

Text 节点由 Text 类型表示,包含按字面解释的纯文本,也可能包含转义后的 HTML 字符,但不 含 HTML 代码。

Text 节点中包含的文本可以通过 nodeValue 属性访问,也可以通过 data 属性访问,这两个属性 包含相同的值。修改 nodeValue 或 data 的值,也会在另一个属性反映出来。文本节点暴露了以下操作文本的方法

appendData(text),向节点末尾添加文本 text;

deleteData(offset, count),从位置 offset 开始删除 count 个字符;

insertData(offset, text),在位置 offset 插入 text;

replaceData(offset, count, text),用 text 替换从位置 offset 到 offset + count 的 文本;

splitText(offset),在位置 offset 将当前文本节点拆分为两个文本节点;

substringData(offset, count),提取从位置 offset 到 offset + count 的文本。

Comment 类型

注释通过 Comment 类型表示。

Comment 类型与 Text 类型继承同一个基类(CharacterData),因此拥有除 splitText()之外 Text 节点所有的字符串操作方法。与 Text 类型相似,注释的实际内容可以通过 nodeValue 或 data 属性获得

注释节点可以作为父节点的子节点来访问。

1
<div id="myDiv"><!-- A comment --></div> 

这里的注释是<div>元素的子节点,这意味着可以像下面这样访问它

1
2
3
let div = document.getElementById("myDiv"); 
let comment = div.firstChild;
alert(comment.data); // "A comment"

CDATASection 类型

待补充

DocumentType 类型

DocumentType 类型的节点包含文档的文档类型(doctype)信息

DocumentType 对象保存在 document.doctype 属性中。DOM Level 1 规定了 DocumentType 对象的 3 个属性:name、entities 和 notations。

其中,name 是文档类型的名称, entities 是这个文档类型描述的实体的 NamedNodeMap,而 notations 是这个文档类型描述的表示 法的 NamedNodeMap。

DocumentFragment 类型

待补充

Attr 类型

待补充

DOM 编程