10 DOM

本章内容

  • 理解包含不同层次节点的 DOM
  • 使用不同的节点类型
  • 克服浏览器兼容性问题及各种陷阱

DOM 是针对 HTML 和 XML 文档的一个 API。DOM 描绘了一个层次化的节点树,允许开发人员添加、移除和修改页面的某一部分。本章主要讨论与浏览器中的 HTML 页面相关的 DOM1 级的特性和应用,以及 JavaScript 对 DOM1 级的实现。

注意,IE 中的所有 DOM 对象都是以 COM 对象的形式实现的。这意味着 IE 中的 DOM 对象与原生 JavaScript 对象的行为活动特点并不一致。

10.1 节点层次

DOM 可以将任何 HTML 或 XML 文档描绘成一个由多层节点构成的结构。

10.1.1 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");
}

由于 IE 没有公开Node类型的构造函数,因此上面的代码在 IE 中会导致错误。为确保跨浏览器兼容,最好还是将nodeType属性与数字值进行比较:

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

并非所有节点类型都受到 Web 浏览器的支持。开发人员最常用的就是元素和文本节点。

  1. nodeName 和 nodeValue 属性
    在使用这俩属性之前,最好先检测一下节点的类型。
if (someNode.nodeType == 1) {
  name = someNode.nodeName;
}

对于元素节点,nodeName中保存的始终都是元素的标签名,而nodeValue的值则始终未null

  1. 节点关系
    每个节点都有一个childNodes属性,其中保存着一个NodeList对象。它是一种类数组对象,用于保存一组有序的节点,可以通过位置来访问这些节点。它实际上是基于 DOM 结构动态执行查询的结果,因此 DOM 结构的变化能够自动反映在NodeList对象中。
var firstChild = someNode.childNodes[0];
var secondChild = someNode.childNodes.item(1);
var count = someNode.childNodes.length;

其中,length属性表示的是访问NodeList的那一刻,其中包含的节点数量。对arguments对象使用Array.prototype.slice()方法可以将其转换为数组。也可以将NodeList对象转换为数组。

//在 IE8 及之前版本中无效
var arrayOfNodes = Array.prototype.slice.call(someNode.childNodes,0);

要想在 IE 中将NodeList转换为数组,必须手动枚举所有成员。

function convertToArray(nodes) {
  var array = null;
  try {
    array = Array.prototype.slice.call(nodes, 0);
  } catch(ex) {
    array = new Array();
    for (var i=0,len=nodes.length;i<len;i++) {
      array.push(nodes[i]);
    }
  }
}

每个节点都有一个parentNode属性,该属性指向文档树中的树节点。包含在childNodes列表中的所有节点都具有相同的父节点,因此它们的parentNode属性都指向同一个节点。此外,包含在childNodes列表中的每个节点相互之间都是同胞节点。可以使用previousSiblingnextSibling属性,可以访问同一列表中的其他节点。

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

父节点的firstChildlastChild属性分别子节点列表中的第一个和最后一个节点。
另外,hasChildNodes()也是一个非常有用的方法,这个方法在节点包含一或多个子节点的情况下返回true
所有节点都有的最后一个属性是ownerDocument,该属性指向表示整个文档的文档节点。这种关系表示的任何节点都属于它所在的文档,任何节点都不能同时存在于两个或更多个文档中。

虽然所有节点类型都继承自Node,但并不是每种节点都有子节点。

  1. 操作节点
    其中,最常用的方法是appendChild(),用于向childNodes列表的末尾添加一个节点。更新完成后,appendChild()返回新增的节点。
var returnedNode = someNode.appendChild(newNode);
alert(returnedNode == newNode);  //true
alert(someNode.lastChild == newNode);  //true

如果传入到appendChild()中的节点已经是文档中一部分了,那结果就是将该节点从原来的位置转移到新位置。
如果需要把节点放在列表中的某个位置,而不是放在末尾,可以使用insertBefore()方法。这个方法接受两个参数:要插入的节点和作为参照的节点。如果参照节点是null,则放在末尾。

//插入后称为最后一个字节点
returnedNode = someNode.insertBefore(newNode, null);
alert(newNode == someNode.lastChild);  //true
//插入后成为第一个子节点
var returnedNode = someNode.insertBefore(newNode, someNode.firstChild);
alert(returnedNode == newNode);  //true
alert(newNode == someNode.firstChild);  //true

replaceChild()方法接受俩参数:要插入的节点和要替换的节点。要替换的节点将由这个方法返回并从文档树中被移除,同时由要插入的节点占据其位置。

//替换第一个子节点
var returnedNode = someNode.replaceChild(newNode,someNode.firstChild);
//替换最后一个节点
returnedNode = someNode.replaceChild(newNode, someNode.lastChild);

如果只想移除而非替换节点,可以使用removeChild()方法。这个方法接受一个参数,即要移除的节点。

//移除第一个子节点
var formerFirstChild = someNode.removeChild(someNode.firstChild);
//移除最后一个子节点
var formerLastChild = someNode.removeChild(someNode.lastChild);

与使用replaceChild()方法一样,通过removeChild()移除的节点仍然为文档所有,只不过在文档中已经没有了自己的位置。
要使用这几个方法必须先取得父节点(使用parentNode属性)。另,并不是所有类型的节点都有子节点。

  1. 其他方法
    有两个方法是所有类型的节点都有的。第一个就是cloneNode(),用于创建调用这个方法的节点的一个完全相同的副本。cloneNode()方法接受一个布尔值参数,表示是否执行深复制。参数为true的情况下,执行深复制,也就是复制节点及其整个子节点树;参数为false的情况下,执行浅复制,即只复制节点本身。复制后返回的节点副本属于文档所有,但并没有为它指定父节点。因此,这个节点副本就成为了一个“孤儿”,除非通过appendChild()insertBefore()replaceChild()将它添加到文档中。

cloneNode()方法不会复制添加到 DOM 节点中的 JavaScript 属性,例如事件处理程序等。IE 在此存在一个 bug,即它会复制事件处理程序,所以我们建议在复制之前最好先移除事件处理程序。

介绍的最后一个方法是normalize(),这个方法唯一的作用就是处理文档树中的文本节点。当在某个节点上调用这个方法时,如果找到了空文本节点,则删除它;如果找到相邻的文本节点,则将它们合并为一个文本节点。

10.1.2 Document 类型

JavaScript 通过Document类型表示文档。document对象是HTMLDocument(继承自 Document 类型)的一个实例,表示整个 HTML 页面。而且,document对象是window对象的一个属性,因此可以将其作为全局对象来访问。Document节点具有下列特征:

  • nodeType的值为 9;
  • nodeName的值为“#document”;
  • nodeValue的值为null
  • parentNode的值为null
  • ownerDocument的值为null
  • 其子节点可能是一个DocumentType(最多一个)、Element(最多一个)、ProcessingInstructionComment

Document类型可以表示 HTML 页面或者其他基于 XML 的文档。不过,最常见的还是作为HTMLDocument实例的document对象。通过这个文档对象,不仅可以取得与页面有关的信息,而且还能操作页面的外观及其底层结构。

  1. 文档的子节点
    有两个内置的访问其子节点的快捷方式。第一个就是documentElement属性,该属性始终指向 HTML 页面中的<html>元素。另一个就是通过childNodes列表访问文档元素,但通过documentElement属性则能更快捷、更直接地访问该元素。如下所示:
var html = document.documentElement;
alert(html === document.childNodes[0]);  //true(false ?)
alert(html === document.firstChild);  //true (false ?)

作为HTMLDocument的实例,document对象还有一个body属性,直接指向<body>元素。开发人员经常要使用这个元素。

var body = document.body;  //取得引用

所有浏览器都支持document.documentElementdocument.body属性。
另一个可能的子节点是DocumentType。通常将<!DOCTYPE>标签看成一个与文档其他部分不同的实体,可以通过doctype属性来访问它的信息。

var doctype = document.doctype;

由于浏览器对document.doctype的支持不一致,因此这个属性的用处很有限。
从技术上说,出现在<html>元素外的注释应该算是文档的子节点。然而,不同的浏览器在是否解析这些注释以及能否正确处理它们等方面,也存在很大差异。

  1. 文档信息
    作为HTMLDocument的一个实例,document对象还有一些标准的Document对象所没有的属性。这些属性提供一些网页的一些信息。
//取得文档标题
var originalTitle = document.title;
//设置文档标题
document.title = "New page title";
//取得完整的 URL
var url = document.URL;
//取得域名
var domain = document.domain;
//取得来源页面的 URL
var referrer = document.referrer;

只有domain是可以设置的。如果 URL 中包含一个子域名,例如 p2p.wrox.com,那么就只能将domain设置为"wrox.com"(URL 中包含“www”,如 www.wrox.com 时,也是如此)。不能将这个属性设置为 URL 中不包含的域。
当页面中包含来自其他子域的框架或内嵌框架时,能够设置document.domain就非常方便了。由于跨域安全限制,来自不同子域的页面无法通过 JavaScript 通信。而通过将每个页面的document.domain设置为相同的值,这些页面就可以互相访问对方包含的JavaScript对象了。

  1. 查找元素
    第一个方法,getElementById,通过 ID 获取该元素。
    另一个常用于取得元素引用的方法是getElementsByTagName,通过标签名取得元素,返回的是包含零或多个元素的NodeList。在 HTML 文档中,这个方法会返回一个 HTMLCollection 对象。
var images = document.getElementByTagName('img');
alert(images.length);
alert(images[0].src);
alert(images.item(0).src);

HTMLCollection对象还有一个方法,namedItem(),使用这个方法可以通过元素的name特性取得集合中的项。

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

还支持按名称访问项。

var myImage = images["myImage"];

HTMLCollection而言,我们可以向方括号中传入数值或字符串形式的索引值。在后台,对数值索引就会调用item(),而对字符串索引就会调用namedItem()
要想取得文档中的所有元素,可以向getElementsByTagName()中传入"*"。在 JavaScript 及 CSS 中,星号(*)通常表示“全部”。

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

第三个方法,也是只有HTMLDocument类型才有的方法,是getElementsByName()

  1. 特殊集合
    还有一些特殊的集合,这些集合都是HTMLCollection对象,为访问文档常用的部分提供了快捷方式。
  2. DOM 一致性检测
    由于 DOM 分为多个级别,也包含多个部分,因此检测浏览器实现了 DOM 的哪些部分就十分必要。DOM1 级只为 document.implementation规定了一个方法,即hasFeatrue()
var hasXmlDom = document.implementation.hasFeature("XML", "3.0");

建议在多数情况下,在使用 DOM 的某些特殊功能之前,最好除了检测hasFeature()之外,还同时使用能力检测。

  1. 文档写入
    这个能力体现在下列 4 个方法中:write()writeln()open()close()writeln()会在字符串的末尾添加一个换行符(\n)。
    方法openclose()分别用于打开和关闭网页的输出流。

严格型 XHTML 文档不支持文档写入。对于那些按照 application/xml + xhtml 内容类型提供的页面,这两个方法也同样无效。

10.1.3 Element 类型

Element类型用于表现 XML 或 HTML 元素。提供了对元素标签名、子节点及特性的访问。Element节点具有以下特征:

  • nodeType的值为 1;
  • nodeName的值为元素的标签名;
  • nodeValue的值为null
  • parentNode可能是DocumentElement
  • 其子节点可能是ElementTextCommentProcessingInstructionCDATASectionEntityReference

要访问元素的标签名,可以使用nodeName属性,也可以使用tagName属性。

var div = document.getElementById('myDiv');
alert(div.tagName);  //"DIV"
alert(div.tagName == div.nodeName);  //true

在 HTML 中,标签名始终都以全部大写表示;而在 XML中,标签名则始终会与源代码中的保持一致。最好是在比较之前将标签名转换为相同的大小写形式。

if (element.tagName.toLowerCase() == "div") {}
  1. HTML 元素
    所有 HTML 元素都由HTMLElement类型表示,不是直接通过这个类型,也是通过它的子类型来表示。HTMLElement类型直接继承自Element并添加了一些属性。添加的这些属性分别对应于每个 HTML 元素中都存在的下列标准特性。
  • id,元素在文档中的唯一标识符。
  • title,有关元素的附加说明信息,一般通过工具提示条显示出来。
  • lang,元素内容的语言代码,很少使用。
  • dir,语言的方向,很少使用。
  • className,与元素的class特性对应,即为元素指定的 CSS 类。class是 ECMAScript 的保留字。
    上述属性都可以用来取得或修改相应的特性值。
var div = document.getElementById('myDiv');
alert(div.id);
alert(div.className);
alert(div.title);
alert(div.lang);
alert(div.dir);
div.id = 'someOtherId';
div.className = 'ft';
div.title = 'Some Other text';
div.lang = 'fr';
div.dir = 'rtl';
  1. 取得特性
    每个元素都有一或多个特性,这些特性的用途是给出相应元素或其内容的附加信息。操作特性的 DOM 方法主要有三个,分别是getAttribute()setArributeremoveAttribute()
var div = document.getElementById('myDiv');
alert(div.getAttribute('id'));
alert(div.getAttribute('class'));
alert(div.getAttribute('title'));
alert(div.getAttribute('lang'));
alert(dib.getAttribute('dir'));

注意,传递给getAttribute()的特性名与实际的特性名相同。因此要想得到class特性值,应该传入class而不是className,后者只有在通过对象属性访问特性时采用。
通过getAttribute()方法也可以取得自定义特性(即标准 HTML 语言中没有的特性)的值。不过,特性的名称是不区分大小写的。另外,根据 HTML5 规范,自定义特性应该加上data-前缀以便验证。
只有公认的(非自定义)特性才会以属性的形式添加到 DOM 对象中。
有两类特殊的特性,它们虽然有对应的属性名,但属性的值与通过getAttribute()返回的值并不相同。第一类特性就是style,用于通过 CSS 为元素指定样式。使用getAttribute()返回的是 CSS 文本,而通过属性访问则会返回一个对象。
第二类是onclick这样的事件处理程序。通过getAttribute()访问时返回的是相应代码的字符串。而在访问onclick属性时,则会返回一个 JavaScript 函数。
由于存在这些差别。经常不使用getAttribute(),而是只使用对象的属性。只有在取得自定义特性值的情况下,才会使用getAttribute()方法。

  1. 设置特性
    setAttribute()该方法接受两个参数:要设置的特性名和值。如果特性已经存在,setAttribute()会以指定的值替换现有的值;如果特性不存在,setAttribute()则创建该属性并设置相应的值。
div.setAttribute('id', 'someOtherId');
div.setAttribute('title', 'Some other text');
div.setAttribute('lang','fr');

直接给属性赋值也可以设置特性的值。

div.id = "someOtherId";

不过,像下面这样为 DOM 元素添加一个自定义的属性,该属性不会自动成为元素的特性。

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

要介绍的最后一个方法是removeAttribute(),这个方法用于彻底删除元素的特性。调用这个方法不仅会清除特性的值,而且也会从元素中完全删除特性。

  1. attributes 属性
    Element类型是使用attributes属性的唯一一个 DOM 节点类型。一般不使用。可以用来遍历节点的特性。
  2. 创建元素
    使用document.createElement()方法可以创建新元素。这个方法只接受一个参数,即标签名。
var div = document.createElement('div');
div.id = "myNewDiv";
div.className = "box";
document.body.appenChild(div);
  1. 元素的子节点
    如果需要通过childNodes属性遍历子节点,需要检查一下nodeType属性。
for (var i=0, len = element.childNodes.length; i++) {
  if (element.childNodes[i].nodeType == 1) {
    //执行某些操作
  }
}

10.1.4 Text 类型

文本节点由Text类型表示,包含的是可以照字面解释的纯文本内容。具有以下特征:

  • nodeType的值为3;
  • nodeName的值为“ #text ”;
  • nodeValue的值为节点所包含的文本;
  • parentNode是一个Element
  • 不支持(没有)子节点。

使用下列方法可以操作节点中的文本。

  • 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为止的字符串。

文本节点和还有一个length属性,保存着节点中字符的数目。

  1. 创建文本节点
    可以使用document.createTextNode()创建文本节点,这个方法接受一个参数--要插入节点中的文本。
var textNode = document.createTextNode("<strong>hello</strong>world!");
var element = document.createElement('div');
element.className = "message";
element.appendChild(textNode);
document.body.appendChild(element);
  1. 规范化文本节点
    DOM 文档中存在相邻的同胞文本节点很容易导致混乱,因为分不清哪个文本节点表示哪个字符串。在一个包含两个或多个文本节点的父元素上调用normalize()方法,则会将所有文本节点合并成一个节点,结果节点的nodeValue等于将合并前每个文本节点的nodeValue值拼接起来的值。
  2. 分割文本节点
    splitText()方法会将一个文本节点分成两个文本节点,即按照指定位置分割nodeValue值。原来的文本节点将包含从开始到指定位置之前的内容,新文本节点将包含剩下的文本。该方法会返回一个新文本节点,该节点与原节点的parentNode相同。
var element = document.createElement('div');
element.className = 'message';
var textNode = document.createTextNode('hello world!');
element.appendChild(textNode);
document.body.appendChild(element);
var newNode = element.firstChild.splitText(5);
alert(element.firstChild.nodeValue);  //"hello"
alert(newNode.nodeValue);  //"world"
alert(element.childNodes.length);  //2

10.1.5 Comment 类型

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

  • nodeType的值为 8;
  • nodeName的值为“ #comment”;
  • nodeValue的值是注释的内容;
  • parentNode可能是DocumentElement;
  • 不支持(没有)子节点。

使用document.createComment()并为其传递注释文本也可以创建注释节点。
显然,开发人员很少会创建和访问注释节点。

10.1.6 CDATASection 类型

CDATASection类型只针对基于 XML 的文档,表示的是 CDATA 区域。与Comment类似,CDATASection类型继承自Text类型,因此拥有除splitText()之外的所有字符串操作方法。
在真正的 XML 文档中,可以使用document.createCDataSection()来创建 CDATA 区域,只需为其传入节点的内容即可。

10.1.7 DocumentType 类型

该类型并不常用。

10.1.8 DocumentFragment 类型

在所有节点类型中,只有DocumentFragment在文档中没有对应的标记。可以将它作为一个“仓库”使用。
如果逐个地添加表项,将会导致浏览器反复渲染(呈现)新信息。可以像下面这样使用一个文档片段来保存创建的列表项,然后再一次性将它们添加到文档中。

var fragment = document.createDocumentFragment();
var ul = document.getElementById('myList');
var li = null;
for (var i=0; i < 3; i++) {
  li = document.createElement('li');
  li.appendChild(document.createTextNode('item'+(i+1)));
  fragment.appendChild(li);
}
ul.appendChild(fragment);

10.1.9 Attr 类型

元素的特性在 DOM 中以Attr类型来表示。从技术角度讲,特性就是存在于元素的attributes属性中的节点。
尽管它们也是节点,但特性却不认为是 DOM 文档树的一部分。最常使用的是getAttribute()setAttribute()removeAttribute()方法,很少直接引用特性节点。

10.2 DOM 操作技术

10.2.1 动态脚本

创建动态脚本也有两种方式:插入外部文件和直接插入 JavaScript 代码。

var script = document.createElement('script');
script.type = "text/javascript";
script.src = "client.js";
document.body.appendChild(script);

整个过程可以使用下面的函数来封装:

function loadScript(url) {
  var script = document.createElement("script");
  script.type = 'text/javascript';
  script.src = url;
  document.body.appendChild(script);
}
function loadScriptString(code) {
  var script = document.createElement('script');
  script.type = 'text/javascript';
  try {
    script.appendChild(document.createTextNode(code));
  } catch (ex) {
    script.text = code;
  }
  document.body.appendChild(script);
}
loadScriptString('function sayHi(){alert('hi');}');

10.2.2 动态样式

动态样式是在页面加载完成后动态添加到页面中的。

function loadStyles(url) {
  var link = document.createElement('link');
  link.rel = 'stylesheet';
  link.type = 'text/css';
  link.href = url;
  var head = document.getElementsByTagName('head')[0];
head.appendChild(link);
}
function loadStylesString(css) {
  var style = document.createElement('style');
  style.type = 'text/css';
  try {
    style.appendChild(document.createTextNode(css));
  } catch (ex) {
    style.styleSheet.cssText = css;
  }
  var head = document.getElementsByTagName('head')[0];
  head.appendChild(style);
}
loadStyleString('body{background-color:red}');

10.2.3 操作表格

为了方便构建表格,HTML DOM 还为<table><tbody><tr>元素添加了一些属性和方法。

10.2.4 使用 NodeList

理解NodeListNamedNodeMapHTMLCollection,是从整体上透彻理解 DOM 的关键所在。这三个集合都是“动态的”,每当文档结构发生变化时,它们都会得到更新。

10.3 小结

DOM 是语言中立的 API,用于访问和操作 HTML 和 XML 文档。 DOM 1 级将 HTML 和 XML 文档形象地看作一个层次化的节点树,可以使用 JavaScript 来操作这个节点树,进而改变底层文档的外观和结构。
DOM 由各种节点构成,简要总结如下。

  • 最基本的节点类型是Node,用于抽象地表示文档中一个独立的部分;所有其他类型都继承自Node
  • Document类型表示整个文档,是一组分层节点的根节点。在 JavaScript 中, document对象是Document的一个实例。使用document对象,有很多种方式可以查询和取得节点。
  • Element节点表示文档中的所有 HTML 或 XML 元素,可以用来操作这些元素的内容和特性。
  • 另外还有一些节点类型,分别表示文本内容、注释、文档类型、CDATA 区域和文档片段。

DOM 操作往往是 JavaScript 程序中开销最大的部分,而因访问NodeList导致的问题为最多。NodeList对象都是“动态的”,这就意味着每次访问NodeList对象,都会运行一次查询。所以,最好的方法就是尽量减少 DOM 操作。

推荐阅读更多精彩内容