JavaScript高级程序设计 第十四章---DOM 文档对象模型

(23) 2024-04-01 09:01:01

第十四章

本章内容

  • 理解文档对象模型(DOM)的构成
  • 节点类型
  • 浏览器兼容性
  • MutationObserver 接口

文档对象模型(DOM,Document Object Model)是HTML 和XML 文档的编程接口。DOM 表示由多层节点构成的文档,通过它开发者可以添加、删除和修改页面的各个部分。脱胎于网景和微软早期的动态HTML(DHTML,Dynamic HTML),DOM 现在是真正跨平台、语言无关的表示和操作网页的方式。

14.1 节点层级

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

<html>
	<head>
		<title>Sample Page</title>
	</head>
	<body>
		<p>Hello World!</p>
	</body>
</html>

表示为层级结构:

JavaScript高级程序设计 第十四章---DOM 文档对象模型 (https://mushiming.com/)  第1张
其中,document 节点表示每个文档的根节点。在这里,根节点的唯一子节点是<html>元素,我们称之为文档元素(documentElement)文档元素是文档最外层的元素,所有其他元素都存在于这个元素之内。每个文档只能有一个文档元素。在HTML 页面中,文档元素始终是<html>元素。
在XML 文档中,则没有这样预定义的元素,任何元素都可能成为文档元素。

HTML 中的每段标记都可以表示为这个树形结构中的一个节点。元素节点表示HTML 元素,属性节点表示属性,文档类型节点表示文档类型,注释节点表示注释。DOM中总共有12 种节点类型,这些类型都继承一种基本类型。

即每个HTML元素,HTML元素内容甚至HTML元素的一个属性都可以被视为一个节点,它们都有不同的Node属性来进行区分。

对节点使用typeof返回是object类型。

对于这样一个元素来说,由以下图示的节点结构组成:

<p title = "a gentle reminder">Don't forget to buy this stuff. </p>

JavaScript高级程序设计 第十四章---DOM 文档对象模型 (https://mushiming.com/)  第2张

14.1.1 Node类型

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

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

1. nodeName 与nodeValue

nodeName 与nodeValue 保存着有关节点的信息。这两个属性的值完全取决于节点类型。

2. 节点关系

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

每个节点都有一个childNodes 属性,其中包含一个NodeList 的实例。NodeList 是一个类数组对象,用于存储可以按位置存取的有序节点。注意,NodeList 并不是Array 的实例,但可以使用中括号访问它的值,而且它也有length 属性。NodeList 对象独特的地方在于,它其实是一个对DOM结构的查询,因此DOM 结构的变化会自动地在NodeList 中反映出来。NodeList 是实时的活动对象,而不是第一次访问时所获得内容的快照。

每个节点都有一个parentNode 属性,指向其DOM 树中的父元素。childNodes 中的所有节点都有同一个父元素,因此它们的parentNode 属性都指向同一个节点。此外,childNodes 列表中的每个节点都是同一列表中其他节点的同胞节点。

使用previousSiblingnextSibling 可以在这个列表的节点间导航。这个列表中第一个节点的previousSibling 属性是null,最后一个节点的nextSibling 属性也是null。

父节点和它的第一个及最后一个子节点也有专门属性:firstChildlastChild 分别指向childNodes 中的第一个和最后一个子节点。

上述这些节点之间的关系为在文档树的节点之间导航提供了方便。下图形象地展示了这些关系。

JavaScript高级程序设计 第十四章---DOM 文档对象模型 (https://mushiming.com/)  第3张
通过这些关系指针,几乎可以访问到文档树中的任何节点。

hasChildNodes()方法用于判断子节点是否存在,如果返回true 则说明节点有一个或多个子节点。

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

3. 操纵节点
  • 最常用的方法是appendChild(),用于在childNodes 列表末尾添加节点。添加新节点会更新相关的关系指针,包括父节点和之前的最后一个子节点。appendChild()方法返回新添加的节点。
let returnedNode = someNode.appendChild(newNode);

alert(returnedNode == newNode); // true
alert(someNode.lastChild == newNode); // true

如果把文档中已经存在的节点传给appendChild(),则这个节点会从之前的位置被转移到新位置。即使DOM 树通过各种关系指针维系,一个节点也不会在文档中同时出现在两个或更多个地方。

  • 如果想把节点放到childNodes 中的特定位置而不是末尾,则可以使用insertBefore()方法。这个方法接收两个参数:要插入的节点和参照节点。调用这个方法后,要插入的节点会变成参照节点的前一个同胞节点,并被返回。如果参照节点是null,则insertBefore()appendChild()效果相同,如下面的例子所示:
// 作为最后一个子节点插入
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

  • replaceChild()方法接收两个参数:要插入的节点和要替换的节点。要替换的节点会被返回并从文档树中完全移除,要插入的节点会取而代之。
// 替换第一个子节点
let returnedNode = someNode.replaceChild(newNode, someNode.firstChild);

使用replaceChild()插入一个节点后,所有关系指针都会从被替换的节点复制过来。被替换的节点从技术上说仍然被同一个文档所拥有,但文档中已经没有它的位置。

  • removeChild()方法。这个方法接收一个参数,即要移除的节点。被移除的节点会被返回。与replaceChild()方法一样,通过removeChild()被移除的节点从技术上说仍然被同一个文档所拥有,但文档中已经没有它的位置。

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

即都是在父节点上对子节点进行的操作。

4. 其他方法

  1. cloneNode()返回与调用它的节点一模一样的节点。cloneNode()方法接收一个布尔值参数,表示是否深复制。在传入true 参数时,会进行深复制,即复制节点及其整个子DOM 树。如果传入false,则只会复制调用该方法的节点。复制返回的节点属于文档所有,但尚未指定父节点,所以可称为孤儿节点(orphan)。可以通过appendChild()、insertBefore()或replaceChild()方法把孤儿节点添加到文档中。

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

14.1.2 Document类型

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

即浏览器中每个HTML都会有一个document对象,而这个document对象就是这里HTMLDocument类型的一个实例。

Document 类型可以表示HTML 页面或其他XML 文档,但最常用的还是通过HTMLDocument 的实例取得document 对象。document 对象可用于获取关于页面的信息以及操纵其外观和底层结构。

1. 文档子节点

提供了两个访问子节点的快捷方式:

  1. documentElement 属性,始终指向HTML 页面中的<html>元素。
<html>
	<body>
	</body>
</html>

这样一个HTML文档,文档只有一个子节点,即<html>元素。这个元素既可以通过
documentElement 属性获取,也可以通过childNodes 列表访问,如下所示:

let html = document.documentElement; // 取得对<html>的引用
alert(html === document.childNodes[0]); // true 
  1. body属性,直接指向<body>元素
let body = document.body; // 取得对<body>的引用
  1. doctype 属性访问文档中独立的<!doctype>标签
 let doctype = document.doctype; // 取得对<!doctype>的引用

而对于<html>元素外面的注释,也是文档的子节点,类型是Comment。不过,由于浏览器实现不同,这些注释不一定能被识别,或者表现可能不一致:

<!-- 第一条注释 -->
<html>
	<body>

	</body>
</html>
<!-- 第二条注释 -->

一般来说,appendChild()、removeChild()和replaceChild()方法不会用在document 对象上。这是因为文档类型(如果存在)是只读的,而且只能有一个Element 类型的子节点(即<html>,已经存在了)

2. 文档信息

document 作为HTMLDocument 的实例,有一些标准Document 对象上所没有的属性。这些属性提供浏览器所加载网页的信息。

  1. title 属性: 包含<title>元素中的文本,通常显示在浏览器窗口或标签页的标题栏。通过这个属性可以读写页面的标题,修改后的标题也会反映在浏览器标题栏上。但是,修改title 属性并不会改变<title>元素。
 // 读取文档标题
let originalTitle = document.title;
// 修改文档标题
document.title = "New page title";
  1. URL属性:包含当前页面的完整URL(地址栏中的URL),可以读取但修改不会改变其值。
  2. domain属性:包含页面的域名,可以读取并修改但不能给这个属性设置URL 中不包含的值。使用<frame>标签时将domain属性设置为相同的值可以使页面之间可以相互访问对方的JavaScript对象实现通信。
    域名修改示例:如果URL 包含子域名如p2p.wrox.com,则可以将domain 设置为"wrox.com"。但是一旦这样修改后,就不能设置回p2p.wrox.com,进行设置会导致错误。
  3. referrer属性:返回载入当前文档的来源文档的URL。 如果当前文档不是通过超级链接访问的,则为null。
    JavaScript高级程序设计 第十四章---DOM 文档对象模型 (https://mushiming.com/)  第4张
    JavaScript高级程序设计 第十四章---DOM 文档对象模型 (https://mushiming.com/)  第5张

如上面这个页面的referrer属性返回的是谷歌,是通过谷歌搜素进入这个页面,所以谷歌页面就是来源文档。

3. 定位元素

getElementById()getElementsByTagName()以及getElementsByName()

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

<div id="myDiv">Some text</div>

可以使用如下代码取得这个元素:

let div = document.getElementById("myDiv"); // 取得对这个<div>元素的引用

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

getElementsByTagName()方法
这个方法接收一个参数,即要获取元素的标签名,返回包含零个或多个元素的NodeList。在HTML 文档中,这个方法返回一个HTMLCollection 对象。

可以通过中括号或item()方法从HTMLCollection中取得特定的元素,并且有length属性:

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

alert(images.length); // 图片数量
alert(images[0].src); // 第一张图片的src 属性
alert(images.item(0).src); // 同上

HTMLCollection 对象还有一个额外的方法namedItem(),可通过标签的name 属性取得某一项的引用。例如,假设页面中包含如下的元素:

<img src="myimage.gif" name="myImage">

那么像这样从images 中取得对这个<img>元素的引用:

let myImage = images.namedItem("myImage");

let myImage = images.namedItem("myImage");

对HTMLCollection 对象而言,中括号既可以接收数值索引,也可以接收字符串索引。而在后台,数值索引会调用item(),字符串索引会调用namedItem()

要取得文档中的所有元素,可以给getElementsByTagName()传入*。在JavaScript 和CSS 中,*一般被认为是匹配一切的字符。来看下面的例子:

let allElements = document.getElementsByTagName("*");

这行代码可以返回包含页面中所有元素的HTMLCollection 对象,顺序就是它们在页面中出现的顺序。因此第一项是<html>元素,第二项是<head>元素,以此类推。

getElementsByName()方法
这个方法会返回具有给定name 属性的所有元素。getElementsByName()方法最常用于单选按钮,因为同一字段的单选按钮必须具有相同的name 属性才能确保把正确的值发送给服务器。同样是返回HTMLCollection对象。

4.特殊集合

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

  • document.anchors 包含文档中所有带name 属性的<a>元素。
  • document.forms 包含文档中所有<form>元素。
  • document.images 包含文档中所有<img>元素。
  • document.links 包含文档中所有带href 属性的<a>元素。

所有HTMLCollection 对象的内容都会实时更新以符合当前文档的内容。

6. 文档写入

向网页输出流中写入内容:4 个方法:write()writeln()open()close()

write()writeln()方法都接收一个字符串参数,可以将这个字符串写入网页中write()简单地写入文本,而writeln()还会在字符串末尾追加一个换行符
(\n)。这两个方法可以用来在页面加载期间向页面中动态添加内容:

<body>
	<p>The current date and time is:
		<script type="text/javascript"> document.write("<strong>" + (new Date()).toString() + "</strong>"); </script>
	</p>
</body>

这个例子会在页面加载过程中输出当前日期和时间,并且会创建一个<strong>元素。

如果是在页面加载完之后再调用document.write(),则输出的内容会重写整个页面。网页显示会变为这个方法中的内容,重写了整个内容,相等于载入了一份新的html文件。

open()close()方法分别用于打开和关闭网页输出流,即终止文档流模型的读取显示。

14.1.3 Element类型

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

  • nodeName 值为元素的标签名;
  • parentNode 值为Document 或Element 对象;

可以通过nodeNametagName 属性来获取元素的标签名。HTML中获取的元素标签名始终以全大写表示。在XML(包括XHTML)中,标签名始终与源代码中的大小写一致。如果不确定脚本是在HTML 文档还是XML 文档中运行,最好将标签名转换为小写形式。

最好用下面的第二种方式使用:

if (element.tagName == "div"){ 
    // 不要这样做,可能出错!
	// do something here
}

if (element.tagName.toLowerCase() == "div"
THE END

发表回复