# DOM

文档对象模型(DOM,Document Object Model)是 HTML 和 XML 文档的编程接口。DOM 表示由多层节点构成的文档,通过它开发者可以添加、删除和修改页面的各个部分。

本质上是允许程序读取和操作页面的内容,结构和样式的页面 API。

# 1、节点层级

任何 HTML 或 XML 文档都可以用 DOM 表示为一个由节点构成的层级结构。节点分很多类型,每种类型对应着文档中不同的信息和(或)标记,也都有自己不同的特性、数据和方法,而且与其他类型有某种关系。这些关系构成了层级,让标记可以表示为一个以特定节点为根的树形结构。

<html>
  <head>
    <title>Sample Page</title>
  </head>
  <body>
    <p>Hello World!</p>
  </body>
</html>
1
2
3
4
5
6
7
8

表现的层级关系为:

  • Document
    • Element html
      • Element head
        • Element title
          • Text Sample Page
      • Element body
        • Element p
          • Text Hello World!

###  1.1 Node 类型

Node 接口在 JavaScript 中被实现为 Node 类型,在除 IE 之外的所有浏览器中都可以直接访问这个类型。在 JavaScript 中,所有节点类型都继承 Node 类型,因此所有类型都共享相同的基本属性和方法。

每个节点都有 nodeType 属性,表示该节点的类型。

节点类型由定义在 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)

节点类型可通过与这些常量比较来确定,比如:

if (someNode.nodeType === Node.ELEMENT_NODE) {
  alert("Node is an element.");
}
1
2
3

(1)nodeName 与 nodeValue

nodeName 与 nodeValue 保存着有关节点的信息。这两个属性的值完全取决于节点类型。在使用这两个属性前,最好先检测节点类型,如下所示:

if (someNode.nodeType == 1) {
  value = someNode.nodeName; // 会显示元素的标签名
}
1
2
3

在这个例子中,先检查了节点是不是元素。如果是,则将其 nodeName 的值赋给一个变量。对元素而言,nodeName 始终等于元素的标签名,而 nodeValue 则始终为 null。

(2)节点关系

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

注意,NodeList 并不是 Array 的实例,但可以使用中括号访问它的值,而且它也有 length 属性。NodeList 对象独特的地方在于,它其实是一个对 DOM 结构的查询,因此 DOM 结构的变化会自动地在 NodeList 中反映出来。我们通常说 NodeList 是实时的活动对象,而不是第一次访问时所获得内容的快照。

访问 NodeList 中元素:

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

转换为数组:

// 第一种方式
let arrayOfNodes = Array.prototype.slice.call(someNode.childNodes, 0);

// 第二种方式
let arrayOfNodes = Array.from(someNode.childNodes);

// 第三种方式
let arrayOfNodes = [...someNode.childNodes];
1
2
3
4
5
6
7
8

节点关系:

节点关系

父关系型:

  • parentNode 表示元素的父节点
  • parentElement 返回元素的父元素节点,与 parentNode 的区别在于,其父节点必须是一个Element,如果不是,则返回 null。

子关系型:

  • childNodes 返回一个即时的NodeList,表示元素的子节点列表,子节点可能会包含文本节点,注释节点等
  • children 一个即时的 HTMLCollection,子节点都是 Element,IE9 以下浏览器不支持 children 属性为只读属性,对象类型为 HTMLCollection
  • firstChild 只读属性返回树中节点的第一个子节点,如果节点是无子节点,则返回 null
  • lastChild 返回当前节点的最后一个子节点。如果父节点为一个元素节点,则子节点通常为一个元素节点,或一个文本节点,或一个注释节点。如果没有子节点,则返回 null
  • hasChildNodes 返回一个布尔值,表明当前节点是否包含有子节点

兄弟关系型:

  • previousSibling 返回当前节点的前一个兄弟节点,没有则返回 null
  • previousElementSibling 返回当前元素在其父元素的子元素节点中的前一个元素节点,如果该元素已经是第一个元素节点,则返回 null,该属性是只读的
  • nextSibling 只读属性,返回其父节点的 childNodes 列表中紧跟在其后面的节点,如果指定的节点为最后一个节点,则返回 null
  • nextElementSibling 返回当前元素在其父元素的子元素节点中的后一个元素节点,如果该元素已经是最后一个元素节点,则返回null,该属性是只读的,注意 IE9 以下浏览器不支持
// 用法
if(someNode.parentNode) {
  console.log('parentNode');
}
// someNode.parentElement;
// someNode.childNodes;
// someNode.children;
// someNode.firstChild;
// someNode.lastChild;
// someNode.previousSibling;
// someNode.previousElementSibling;
// someNode.nextElementSibling;
1
2
3
4
5
6
7
8
9
10
11
12

(3)操纵节点

  • appendChild() 用于在 childNodes 列表末尾添加节点
  • insertBefore() 用来添加一个节点到一个参照节点之前
  • removeChild() 删除指定的子节点并返回
  • replaceChild() 用于使用一个节点替换另一个节点
parent.appendChild(child); // parent 表示父节点,child 表示需要添加的节点
parentNode.insertBefore(newNode,refNode); // newNode 表示要添加的节点,refNode 表示参照节点
parent.removeChild(node); // node 表示需要删除的节点
parent.replaceChild(newChild,oldChild); // newChild 是替换的节点,oldChild 是被替换的节点
1
2
3
4

注意:appendChild() 方法如果被添加的节点是一个页面中存在的节点,则执行后这个节点将会添加到指定位置,其原本所在的位置将移除该节点,也就是说不会同时存在两个该节点在页面上,相当于把这个节点移动到另一个地方。如果 child 绑定了事件,被移动时,它依然绑定着该事件。

(4)其他方法

  • cloneNode() 返回与调用它的节点一模一样的节点
  • normalize() 这个方法唯一的任务就是处理文档子树中的文本节点
// 接收一个布尔值参数,表示是否深复制。
// 在传入 true 参数时,会进行深复制,即复制节点及其整个子 DOM 树。
// 如果传入 false,则只会复制调用该方法的节点。
let deepList = myList.cloneNode(true);
1
2
3
4

cloneNode() 方法不会复制添加到 DOM 节点的 JavaScript 属性,比如事件处理程序。这个方法只复制 HTML 属性,以及可选地复制子节点。除此之外则一概不会复制。在 IE 下可能出现意外。所以推荐在复制前先删除事件处理程序。

# 1.2 Document 类型

Document 类型是 JavaScript 中表示文档节点的类型。在浏览器中,文档对象 document 是 HTMLDocument 的实例(HTMLDocument 继承 Document),表示整个 HTML 页面。document 是 window 对象的属性,因此是一个全局对象。

  • nodeType 等于 9;
  • nodeName 值为"#document";
  • nodeValue 值为 null;
  • parentNode 值为 null;
  • ownerDocument 值为 null;
  • 子节点可以是 DocumentType(最多一个)、Element(最多一个)、ProcessingInstruction 或 Comment 类型。

(1)文档子节点

let html = document.documentElement; // 取得对<html>的引用
let body = document.body; // 取得对<body>的引用
let doctype = document.doctype; // 取得对<!doctype>的引用
1
2
3

(2)文档信息

// 读取文档标题
let originalTitle = document.title;
// 修改文档标题
document.title = "New page title";

// 取得完整的 URL 
let url = document.URL;
// 取得域名
let domain = document.domain; 
// 取得来源
let referrer = document.referrer;
1
2
3
4
5
6
7
8
9
10
11

(3)定位元素

  • getElementById() 根据元素ID查找元素,如果没找到则返回 null
  • getElementsByTagName() 根据元素的标签名来查找元素,返回包含零个或多个元素的 NodeList。
  • getElementsByName() 这个方法会返回具有给定 name 属性的所有元素
let div = document.getElementById("myDiv"); // 取得对这个<div>元素的引用
1

(4)文档写入

  • write() 接收一个字符串参数,可以将这个字符串写入网页中
  • writeln() 接收一个字符串参数,可以将这个字符串写入网页中,并且末尾追加一个换行符(\n)
  • open() 用于打开网页输出流
  • close() 用于关闭网页输出流
document.write("hello world"); 
1

严格的 XHTML 文档不支持文档写入。对于内容类型为 application/xml+xhtml 的页面,这些方法不起作用。

# 1.3 Element 类型

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

  • nodeType 等于 1;
  • nodeName 值为元素的标签名;
  • nodeValue 值为 null;
  • parentNode 值为 Document 或 Element 对象;
  • 子节点可以是 Element、Text、Comment、ProcessingInstruction、CDATASection、EntityReference 类型。

可以通过 nodeName 或 tagName 属性来获取元素的标签名。

(1)HTML 元素

标准属性:

  • id,元素在文档中的唯一标识符;
  • title,包含元素的额外信息,通常以提示条形式展示;
  • lang,元素内容的语言代码(很少用);
  • dir,语言的书写方向("ltr"表示从左到右,"rtl"表示从右到左,同样很少用);
  • className,相当于 class 属性,用于指定元素的 CSS 类(因为 class 是 ECMAScript 关键字,所以不能直接用这个名字)。

如前所述,所有 HTML 元素都是 HTMLElement 或其子类型的实例。详见所有 HTML 元素 (opens new window)

(2)属性操作

  • getAttribute() 获取属性
  • setAttribute() 设置属性
  • removeAttribute() 删除属性
let div = document.getElementById("myDiv");
console.log(div.getAttribute("id"));
1
2

(3)attributes 属性

attributes 属性包含一个 NamedNodeMap 实例,是一个类似 NodeList 的“实时”集合。

  • getNamedItem(name),返回 nodeName 属性等于 name 的节点;
  • removeNamedItem(name),删除 nodeName 属性等于 name 的节点;
  • setNamedItem(node),向列表中添加 node 节点,以其 nodeName 为索引;
  • item(pos),返回索引位置 pos 处的节点。
let id = element.attributes.getNamedItem("id").nodeValue;
// 简写的方式
let id = element.attributes["id"].nodeValue;
1
2
3

(4)创建元素

可以使用 document.createElement() 方法创建新元素。

let div = document.createElement("div");
// 设置属性
div.id = "myNewDiv"; 
div.className = "box";
1
2
3
4

# 1.4 Text 类型

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

  • nodeType 等于 3;
  • nodeName 值为"#text";
  • nodeValue 值为节点中包含的文本;
  • parentNode 值为 Element 对象;
  • 不支持子节点。

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 的文本。

文本节点方法:

  • createTextNode() 可以用来创建新文本节点,它接收一个参数,即要插入节点的文本。
  • normalize() 在包含两个或多个相邻文本节点的父节点上调用 normalize()时,所有同胞文本节点会被合并为一个文本节点
  • splitText() 在指定的偏移位置拆分 nodeValue,将一个文本节点拆分成两个文本节点。
let element = document.createElement("div"); 
element.className = "message"; 

let textNode = document.createTextNode("Hello world!"); // 创建文本节点
element.appendChild(textNode);

let anotherTextNode = document.createTextNode("Yippee!"); 
element.appendChild(anotherTextNode);

alert(element.childNodes.length); // 2 
element.normalize(); // 合并
alert(element.childNodes.length); // 1 
alert(element.firstChild.nodeValue); // "Hello world!Yippee!" 

let newNode = element.firstChild.splitText(5); // 拆分
alert(element.firstChild.nodeValue); // "Hello" 
alert(newNode.nodeValue); // " world!Yippee!"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 1.5 Comment 类型

DOM 中的注释通过 Comment 类型表示。Comment 类型的节点具有以下特征:

  • nodeType 等于 8;
  • nodeName 值为"#comment";
  • nodeValue 值为注释的内容;
  • parentNode 值为 Document 或 Element 对象;
  • 不支持子节点。

Comment 类型与 Text 类型继承同一个基类(CharacterData),因此拥有除 splitText() 之外 Text 节点所有的字符串操作方法。

注释的实际内容可以通过 nodeValue 或 data 属性获得。

<div id="myDiv"><!-- A comment --></div>
<script>
  let div = document.getElementById("myDiv"); 
  let comment = div.firstChild; 
  alert(comment.data); // "A comment"

  // 使用 createComment() 创建注释节点
  let comment = document.createComment("A comment"); 
</script>
1
2
3
4
5
6
7
8
9

# 1.6 CDATASection 类型

CDATASection 类型表示 XML 中特有的 CDATA 区块。CDATASection 类型继承 Text 类型,因此拥有包括 splitText() 在内的所有字符串操作方法。

  • nodeType 等于 4;
  • nodeName 值为"#cdata-section";
  • nodeValue 值为 CDATA 区块的内容;
  • parentNode 值为 Document 或 Element 对象;
  • 不支持子节点。

CDATA 区块只在 XML 文档中有效,因此某些浏览器比较陈旧的版本会错误地将 CDATA 区块解析为 Comment 或 Element。

在真正的 XML 文档中,可以使用 document.createCDataSection() 并传入节点内容来创建 CDATA 区块。

### 1.7 DocumentType 类型

DocumentType 类型的节点包含文档的文档类型(doctype)信息,具有以下特征:

  • nodeType 等于 10;
  • nodeName 值为文档类型的名称;
  • nodeValue 值为 null;
  • parentNode 值为 Document 对象;
  • 不支持子节点。

DocumentType 对象在 DOM Level 1 中不支持动态创建,只能在解析文档代码时创建。

DocumentType 对象的 3 个属性:

  • name 文档类型的名称
  • entities 文档类型描述的实体的 NamedNodeMap
  • notations 文档类型描述的表示法的 NamedNodeMap

因为浏览器中的文档通常是 HTML 或 XHTML 文档类型,所以 entities 和 notations 列表为空。

无论如何,只有 name 属性是有用的。这个属性包含文档类型的名称,即紧跟在<!DOCTYPE 后面的那串文本。

<!DOCTYPE HTML PUBLIC "-// W3C// DTD HTML 4.01// EN" 
 "http:// www.w3.org/TR/html4/strict.dtd"> 
<script>
  alert(document.doctype.name); // "html" 
</script>
1
2
3
4
5

# 1.8 DocumentFragment 类型

在所有节点类型中,DocumentFragment 类型是唯一一个在标记中没有对应表示的类型。DOM 将文档片段定义为“轻量级”文档,能够包含和操作节点,却没有完整文档那样额外的消耗。

  • nodeType 等于 11;
  • nodeName 值为"#document-fragment";
  • nodeValue 值为 null;
  • parentNode 值为 null;
  • 子节点可以是 Element、ProcessingInstruction、Comment、Text、CDATASection 或 EntityReference。

不能直接把文档片段添加到文档。相反,文档片段的作用是充当其他要被添加到文档的节点的仓库。可以使用 document.createDocumentFragment() 方法像下面这样创建文档片段:

let fragment = document.createDocumentFragment();
let ul = document.getElementById("myList");

for (let i = 0; i < 3; ++i) { 
  let li = document.createElement("li"); 
  li.appendChild(document.createTextNode(`Item ${i + 1}`)); 
  fragment.appendChild(li); 
}
ul.appendChild(fragment);
1
2
3
4
5
6
7
8
9

# 1.9 Attr 类型

元素数据在 DOM 中通过 Attr 类型表示。Attr 类型构造函数和原型在所有浏览器中都可以直接访问。技术上讲,属性是存在于元素 attributes 属性中的节点。Attr 节点具有以下特征:

  • nodeType 等于 2;
  • nodeName 值为属性名;
  • nodeValue 值为属性值;
  • parentNode 值为 null;
  • 在 HTML 中不支持子节点;
  • 在 XML 中子节点可以是 Text 或 EntityReference。

属性节点尽管是节点,却不被认为是 DOM 文档树的一部分。Attr 节点很少直接被引用,通常开发者更喜欢使用 getAttribute()、removeAttribute() 和 setAttribute() 方法操作属性。

let attr = document.createAttribute("align"); 
attr.value = "left"; 
element.setAttributeNode(attr);
1
2
3

# 2、DOM 编程

# 2.1 动态脚本

两种方式通过<script>动态为网页添加脚本:引入外部文件和直接插入源代码。

<script src="foo.js"></script>
1

或者:

let script = document.createElement("script"); 
script.src = "foo.js"; 
document.body.appendChild(script);
1
2
3

# 2.2 动态样式

CSS 样式在 HTML 页面中可以通过两个元素加载。<link> 元素用于包含 CSS 外部文件,而<style> 元素用于添加嵌入样式。

<link rel="stylesheet" type="text/css" href="styles.css">
<!-- 或者 -->
<style type="text/css"> 
body { 
 background-color: red; 
} 
</style> 
1
2
3
4
5
6
7

使用js创建:

let link = document.createElement("link"); 
link.rel = "stylesheet"; 
link.type = "text/css"; 
link.href = "styles.css"; 
let head = document.getElementsByTagName("head")[0]; 
head.appendChild(link); 
1
2
3
4
5
6

对于 IE,要小心使用 styleSheet.cssText。如果重用同一个 <style> 元素并设置该属性超过一次,则可能导致浏览器崩溃。同样,将 cssText 设置为空字符串也可能导致浏览器崩溃。

# 2.3 操作表格

表格是 HTML 中最复杂的结构之一。通过 DOM 编程创建<table>元素,通常要涉及大量标签,包括表行、表元、表题,等等。因此,通过 DOM 编程创建和修改表格时可能要写很多代码。

<table>元素添加了以下属性和方法:

  • caption,指向<caption>元素的指针(如果存在);
  • tBodies,包含<tbody>元素的 HTMLCollection;
  • tFoot,指向<tfoot>元素(如果存在);
  • tHead,指向<thead>元素(如果存在);
  • rows,包含表示所有行的 HTMLCollection;
  • createTHead(),创建<thead>元素,放到表格中,返回引用;
  • createTFoot(),创建<tfoot>元素,放到表格中,返回引用;
  • createCaption(),创建<caption>元素,放到表格中,返回引用;
  • deleteTHead(),删除<thead>元素;
  • deleteTFoot(),删除<tfoot>元素;
  • deleteCaption(),删除<caption>元素;
  • deleteRow(pos),删除给定位置的行;
  • insertRow(pos),在行集合中给定位置插入一行。

<tbody>元素添加了以下属性和方法:

  • rows,包含<tbody>元素中所有行的 HTMLCollection;
  • deleteRow(pos),删除给定位置的行;
  • insertRow(pos),在行集合中给定位置插入一行,返回该行的引用。

<tr>元素添加了以下属性和方法:

  • cells,包含<tr>元素所有表元的 HTMLCollection;
  • deleteCell(pos),删除给定位置的表元;
  • insertCell(pos),在表元集合给定位置插入一个表元,返回该表元的引用。

# 2.4 使用 NodeList

一般来说,最好限制操作 NodeList 的次数。因为每次查询都会搜索整个文档,所以最好把查询到 的 NodeList 缓存起来。

let divs = document.getElementsByTagName("div"); 
for (let i = 0, len = divs.length; i < len; ++i) { 
  let div = document.createElement("div"); 
  document.body.appendChild(div); 
} 
1
2
3
4
5

# 3、MutationObserver 接口

MutationObserver 接口可以在 DOM 被修改时异步执行回调。

使用 MutationObserver 可以观察整个文档、DOM 树的一部分,或某个元素。此外还可以观察元素属性、子节点、文本,或者前三者任意组合的变化。

新引进 MutationObserver 接口是为了取代废弃的 MutationEvent。

MutationObserver 的实例要通过调用 MutationObserver 构造函数并传入一个回调函数来创建:

let observer = new MutationObserver(() => console.log('DOM was mutated!')); 
1

(1)observe() 方法

新创建的 MutationObserver 实例不会关联 DOM 的任何部分。要把这个 observer 与 DOM 关联起来,需要使用 observe() 方法。

let observer = new MutationObserver(() => console.log('<body> attributes changed')); 
observer.observe(document.body, { attributes: true }); 
document.body.className = 'foo'; 
console.log('Changed body class'); 
// Changed body class 
// <body> attributes changed
1
2
3
4
5
6

执行以上代码后,<body>元素上任何属性发生变化都会被这个 MutationObserver 实例发现,然后就会异步执行注册的回调函数。<body>元素后代的修改或其他非属性修改都不会触发回调进入任务队列。

注意,回调中的 console.log()是后执行的。这表明回调并非与实际的 DOM 变化同步执行。

(2)回调与 MutationRecord

每个回调都会收到一个 MutationRecord 实例的数组。MutationRecord 实例包含的信息包括发生了什么变化,以及 DOM 的哪一部分受到了影响。

(3)disconnect() 方法

默认情况下,只要被观察的元素不被垃圾回收,MutationObserver 的回调就会响应 DOM 变化事件,从而被执行。要提前终止执行回调,可以调用 disconnect() 方法。

上次更新: 3/9/2022, 6:56:39 PM