十、文档对象模型

文档对象模型或 DOM 是一个编程接口,它描述了 web 文档的所有元素以及它们之间的相互关系。通过这个接口,JavaScript 代码可以与 web 文档进行交互。

DOM 节点

在 DOM 模型中,web 文档的内容以由节点组成的树状结构表示。通过了解这些节点是如何组织的,就有可能动态地更改文档的任何部分。考虑下面的 HTML 标记。

<p>My paragraph</p>

这个标记在 DOM 树中创建了两个节点:一个元素节点和一个文本节点。元素节点可以有子节点,这里的文本节点是段落元素的子节点。相反,段落节点是文本节点的父节点。转到下一个示例,有四个节点。

<div id="mydiv">

<!-- My comment -->

</div>

div 元素节点包含三个节点:两个文本节点和一个注释节点。额外的文本节点来自本例中注释前后的空格(空格、制表符和换行符)。查看网页时,这四个节点都是不可见的。

每个元素可以有一个可选的 id 属性,在本例中应用于 div 元素。因为这个属性必须是惟一的,所以它提供了一种在 DOM 树中选择特定节点的便捷方式。注意,DOM 规范也认为属性是节点;但是,在导航 DOM 树时,属性节点被视为元素节点的属性。

选择节点

document 对象表示 web 文档,并提供了几种访问 DOM 树和检索节点引用的方法。最常见的方法是使用getElementById方法。此方法返回对具有指定唯一 id 的元素的引用。使用这个方法,可以检索前面示例中的 div 元素节点。

var mydiv = document.getElementById("mydiv");

也可以用getElementsByTagName方法通过标签名选择元素。此方法检索该类型的所有元素的类似数组的集合。

var mydivs = document.getElementsByTagName("div");

另一种不太常见的选择元素的方式是通过class属性。由于多个元素可以共享同一个类名,因此该方法也返回一个集合。除 IE < 9 之外,所有现代浏览器都支持它。

var myclasses = document.getElementsByClassName("myclass");

getElementById方法只能从文档对象中调用,但是另外两个方法可以从 DOM 树中的特定节点调用,以便只搜索该节点的子节点。例如,下面的代码检索 body 元素下的所有元素节点。

var myelements = document.body.getElementsByTagName("*");

除了 body 节点,document 对象还具有检索对htmlhead节点的引用的属性。

var htmlnode = document.documentElement;

var headnode = document.head;

var bodynode = document.body;

也可以使用querySelector方法定位元素节点。该方法返回匹配指定 CSS 选择器的第一个元素,在本例中是 class 属性设置为myclass的第一个元素。

var mynode = document.querySelector(".myclass");

类似地,querySelectorAll方法检索匹配给定 CSS 查询的所有元素节点的集合。除了 IE < 9,所有现代浏览器都支持这两种查询方式。

var mynodes = document.querySelectorAll(".myclass");

如果找不到元素,则返回 null。这种行为在所有 DOM 方法中都是一致的。

遍历 DOM 树

一旦选择了一个节点,就有许多属性允许相对于该节点遍历 DOM 树。下面的列表可以用来说明。

<ul id="mylist">

<li>First</li>

<li>Second</li>

<li>Third</li>

</ul>

对这个无序列表节点的引用是通过它的 id 来检索的。

var mylist = document.getElementById("mylist");

此节点下的元素节点可通过 children 集合获得。第一个列表项也可以使用firstElementChild属性来检索。

var first = mylist.children[0];

first = mylist.firstElementChild;

同样,可以通过 children 集合或lastElementChild属性访问最后一个节点。

var third = mylist.children[2];

third = mylist.lastElementChild;

可以使用previousElementSiblingnextElementSibling属性将 DOM 树左右导航到相邻节点。在这种情况下,兄弟指的是共享同一个父节点的节点。

var second = first.nextElementSibling;

second = third.previousElementSibling;

这四个属性——firstElementChildlastElementChildnextElementSiblingpreviousElementSibling——除了 IE < 9,所有现代浏览器都支持。对于完整的浏览器支持,可以使用以下属性来代替:firstChildlastChildnextSiblingpreviousSibling。请记住,这些属性也考虑文本和注释节点,而不仅仅是元素节点。

parentNode属性引用父节点。与其他属性一起,它们允许在所有四个方向上遍历 DOM 树:上、下、左、右。

mylist = first.parentNode;

子集合只包含元素节点。一个例外是在 9 之前的 IE 版本中,这个集合也包括注释节点。如果需要所有节点类型,则使用childNodes集合。该集合包含所有子节点,包括元素、文本和注释节点。再一次,IE < 9 的行为与其他浏览器不同,在childNodes集合中没有包含纯空白的文本节点。

mylist.childNodes[0]; // whitespace text node

mylist.childNodes[1]; // li node

mylist.childNodes[2]; // whitespace text node

对于元素节点,nodeNametagName属性都包含大写字母的标签名称。它们可以用来测试元素节点的标记名。

if (mydiv.nodeName == "DIV")

console.log(mydiv.tagName); // "DIV"

虽然tagName属性专门用于元素节点,但是nodeName属性对于任何节点类型都是有用的。例如,注释节点的计算结果为“#comment”,文本节点的计算结果为“#text”。

mylist.childNodes[0].nodeName; // #text

mylist.childNodes[1].nodeName; // LI

mylist.childNodes[2].nodeName; // #text

创建节点

DOM 中的节点可以动态地添加、删除或更改。举例来说,一个新的列表项将被添加到先前的列表中。第一步是分别使用createElementcreateTextNode方法创建元素和文本节点。

var myitem = document.createElement('li');

var mytext = document.createTextNode("New list item");

然后,使用 list item 节点上的appendChild方法,将文本节点添加到元素节点。

myitem.appendChild(mytext);

接下来,使用相同的方法将列表项节点添加到无序列表中。这将导致新列表项出现在页面上。

mylist.appendChild(myitem);

appendChild方法添加它的节点参数作为调用它的元素节点的最后一个子节点。为了在其他地方插入节点,使用了insertBefore方法。此方法采用第二个节点参数,在该参数之前插入新节点。

mylist.insertBefore(myitem, mylist.children[0]);

一个节点不能同时出现在两个地方,因此该操作会删除之前添加到列表末尾的节点,而将其放在列表的开头。要添加另一个类似的节点,可以首先使用cloneNode方法复制元素。此方法采用一个布尔参数,该参数指定元素的后代节点是否也将被复制。

var newitem = myitem.cloneNode(false);

接下来,这个节点被添加到列表的末尾,但是因为它是在没有后代的情况下克隆的,所以它没有文本节点,所以在文档中显示为空列表元素。

mylist.appendChild(newitem);

如前所示,可以使用createTextNodeappendChild方法创建和链接文本节点。一个更短的替代方法是修改innerHTML属性,它代表元素的 HTML 内容。

newitem.innerHTML = "<b>Another</b> new list item";

该属性允许自动创建元素和文本节点。另一个有用的类似属性是textContent。此属性表示去除了任何 HTML 标记的元素内容。

newitem.textContent; // "Another new list item"

newitem.innerHTML;   // "<b>Another</b> new list item"

删除节点

可以使用removeChild方法删除一个节点。该方法返回对已移除节点的引用:在本例中,是列表中的最后一个子节点。请记住,在 JavaScript 中捕捉返回值是可选的。

var removedNode = mylist.removeChild(mylist.lastElementChild);

另一种删除节点的方法是用不同的节点替换它。这是通过replaceChild方法完成的,该方法也返回被替换的节点。下面的代码用以前移除的节点替换列表的第一个子节点。

mylist.replaceChild(removedNode, mylist.firstElementChild);

属性节点

属性节点可以通过其包含元素来访问,而不是作为该元素的子节点。为了便于说明,这里有一个带有id属性的段落,以便于选择。

<p id="myid">My paragraph</p>

它的元素节点以熟悉的方式选择。

var mypara = document.getElementById("myid");

setAttribute方法向被引用的元素添加一个属性,或者替换它的值,如果它已经存在的话。它有两个参数:属性和值。

mypara.setAttribute("class","myclass");

为了检索属性的值,使用了getAttribute方法。在检索属性之前,这里使用hasAttribute方法执行检查以确保它存在。

if (mypara.hasAttribute("class"))

console.log(mypara.getAttribute("class")); // "myclass"

属性自动与元素节点的属性同步。这为设置和获取属性提供了一种不太冗长的方式。属性和它们对应的属性共享相同的名称,除了 class 属性的属性名为className。这是因为 class 是 JavaScript 中的保留关键字。

console.log(mypara.id);        // "myid"

console.log(mypara.className); // "myclass"

使用style属性可以动态改变元素的 CSS 属性。一种方法是直接修改属性。这将覆盖以前通过该属性设置的任何内联样式。

mypara.setAttribute("style", "background-color: yellow;");

相反,要给元素添加新的样式,可以改变style对象的属性。这些属性与其对应的 CSS 属性具有相同的名称,只是删除了所有连字符,并且第一个单词后面的每个单词都大写。

mypara.style.backgroundColor = "yellow";

要恢复样式更改,您只需通过将该属性设置为空字符串来清除它。

mypara.style.backgroundColor = "";