14 DOM

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

DOM Level 1 在 1998 年成为 W3C 推荐标准,提供了基本文档结构和查询的接口。本章之所以介绍DOM,主要因为它与浏览器中的 HTML 网页相关,并且在 JavaScript 中提供了 DOM API。

1 节点层级

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

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

如果表示为层级结构,则如图 14-1 所示。
在这里插入图片描述
其中,document 节点表示每个文档的根节点。在这里,根节点的唯一子节点是<html>元素,我们称之为文档元素(documentElement)。文档元素是文档最外层的元素,所有其他元素都存在于这个元素之内。每个文档只能有一个文档元素。在 HTML 页面中,文档元素始终是<html>元素。在 XML 文档中,则没有这样预定义的元素,任何元素都可能成为文档元素。

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

1.1 Node类型

DOM Level 1 描述了名为 Node 的接口,这个接口是所有 DOM 节点类型都必须实现的。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."); 
}

这个例子比较了 someNode.nodeType 与 Node.ELEMENT_NODE 常量。如果两者相等,则意味着someNode 是一个元素节点。

浏览器并不支持所有节点类型。开发者最常用到的是元素节点和文本节点。本章后面会讨论每种节点受支持的程度及其用法。

1.1.1 nodeName 与 nodeValue

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

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

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

1.1.2 节点关系

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

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

下面的例子展示了如何使用中括号或使用item()方法访问NodeList中的元素:

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

无论是使用中括号还是 item()方法都是可以的,但多数开发者倾向于使用中括号,因为它是一个类数组对象。注意,length 属性表示那一时刻 NodeList 中节点的数量。使用 Array.prototype. slice()可以像前面介绍 arguments 时一样把 NodeList 对象转换为数组。比如:

let arrayOfNodes = Array.prototype.slice.call(someNode.childNodes,0); 

当然,使用 ES6 的 Array.from()静态方法,可以替换这种笨拙的方式:

let arrayOfNodes = Array.from(someNode.childNodes);

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

if (someNode.nextSibling === null){ 
 	alert("Last node in the parent's childNodes list."); 
} else if (someNode.previousSibling === null){ 
 	alert("First node in the parent's childNodes list."); 
} 

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

父节点和它的第一个及最后一个子节点也有专门属性:firstChild 和 lastChild 分别指向childNodes 中的第一个和最后一个子节点。someNode.firstChild 的值始终等于 someNode. childNodes[0],而 someNode.lastChild 的值始终等于 someNode.childNodes[someNode.childNodes.length-1]。如果只有一个子节点,则 firstChild 和 lastChild 指向同一个节点。如
果没有子节点,则 firstChild 和 lastChild 都是 null。上述这些节点之间的关系为在文档树的节点之间导航提供了方便。图 14-2 形象地展示了这些关系。

在这里插入图片描述
有了这些关系,childNodes 属性的作用远远不止是必备属性那么简单了。这是因为利用这些关系指针,几乎可以访问到文档树中的任何节点,而这种便利性是 childNodes 的最大亮点。还有一个便利的方法是 hasChildNodes(),这个方法如果返回 true 则说明节点有一个或多个子节点。相比查询childNodes 的 length 属性,这个方法无疑更方便。

最后还有一个所有节点都共享的关系。ownerDocument 属性是一个指向代表整个文档的文档节点的指针。所有节点都被创建它们(或自己所在)的文档所拥有,因为一个节点不可能同时存在于两个或者多个文档中。这个属性为迅速访问文档节点提供了便利,因为无需在文档结构中逐层上溯了。

注意: 虽然所有节点类型都继承了 Node,但并非所有节点都有子节点。本章后面会讨论不同节点类型的差异。

1.1.3 操纵节点

因为所有关系指针都是只读的,所以 DOM 又提供了一些操纵节点的方法。最常用的方法是appendChild(),用于在 childNodes 列表末尾添加节点。添加新节点会更新相关的关系指针,包括父节点和之前的最后一个子节点。appendChild()方法返回新添加的节点,如下所示:

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

待补充 430

1.2 Document类型

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

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

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

1.2.1 文档子节点

虽然 DOM 规范规定 Document 节点的子节点可以是 DocumentType、Element、ProcessingInstruction 或 Comment,但也提供了两个访问子节点的快捷方式。第一个是 documentElement 属性,始终指向 HTML 页面中的<html>元素。虽然 document.childNodes 中始终有<html>元素,但使用 documentElement 属性可以更快更直接地访问该元素。假如有以下简单的页面:

<html> 
 	<body>
 	 
 	</body> 
</html> 

浏览器解析完这个页面之后,文档只有一个子节点,即元素。这个元素既可以通过 documentElement 属性获取,也可以通过 childNodes 列表访问,如下所示:

let html = document.documentElement; // 取得对<html>的引用
alert(html === document.childNodes[0]); // true 
alert(html === document.firstChild); // true

这个例子表明 documentElement、firstChild 和 childNodes[0]都指向同一个值,即<html>元素。

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

let body = document.body; // 取得对<body>的引用

所有主流浏览器都支持 document.documentElement 和 document.body。

Document 类型另一种可能的子节点是 DocumentType。<!doctype>标签是文档中独立的部分,其信息可以通过 doctype 属性(在浏览器中是 document.doctype)来访问,比如:

let doctype = document.doctype; // 取得对<!doctype>的引用

另外,严格来讲出现在元素外面的注释也是文档的子节点,它们的类型是 Comment。不过,由于浏览器实现不同,这些注释不一定能被识别,或者表现可能不一致。比如以下 HTML 页面:

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

这个页面看起来有 3 个子节点:注释、<html>元素、注释。逻辑上讲,document.childNodes应该包含 3 项,对应代码中的每个节点。但实际上,浏览器有可能以不同方式对待<html>元素外部的注释,比如忽略一个或两个注释。

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

1.2.2 文档信息
1.3 Element 类型
1.3.5 创建元素

可以使用 document.createElement()方法创建新元素。这个方法接收一个参数,即要创建元素的标签名。在 HTML 文档中,标签名是不区分大小写的,而 XML 文档(包括 XHTML)是区分大小写的。要创建<div>元素,可以使用下面的代码:

let div = document.createElement("div"); 

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

div.id = "myNewDiv"; 
div.className = "box"; 

在新元素上设置这些属性只会附加信息。因为这个元素还没有添加到文档树,所以不会影响浏览器显示。要把元素添加到文档树,可以使用 appendChild()、insertBefore()或 replaceChild()。比如,以下代码会把刚才创建的元素添加到文档的<body>元素中:

document.body.appendChild(div);

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值