• DOM(文档对象模型)是针对HTML和XML文档的一个API(应用程序编程接口)。DOM描绘了一个层次化的节点树,允许开发人员添加、移除和修改页面的某一部分。DOM脱胎于Netscape及微软公司创始的DHTML(动态HTML),但现在它已经成为表现和操作页面标记的真正跨平台、语言中立方式。
  • 1998年10月DOM 1 级规范成为W3C的推荐标准,为基本的文档结构及查询提供了接口。本章主要讨论与浏览器中的HTML页面相关的DOM1级的特性和应用,以及JavaScript对DOM1级的视线。
  • IE中的所有DOM对象都是以COM对象的形式实现的。这意味着IE中的DOM对象与原生JavaScript对象的行为或活动特点并不一致。本章将较多的谈及这些差异。

节点层次

<html>
  <head>
    <title>Sample Page</title>
  </head>
  <body>
    <p>Hello World!</p>
  </body>
</html>
  • 可以将这个简单的HTML文档视为一个层次结构,如图10-1所示。

![alt](img/charptor10_1.jpg)

  • 文档节点是每个文档的根节点。在这个例子中,文档节点只有一个子节点,既<html>元素,我们称之为文档元素
  • 文档元素是文档的最外层元素,文档中的其他所有元素都包含在文档元素中。每个文档只能有一个文档元素。在HTML页面中,文档元素始终都是<html>元素。在XML中,没有预定义的元素,因此任何元素都可能成为文档元素。

Node 类型

  • DOM1级定义了一个 Node 接口,该接口将由 DOM 中所有节点类型实现。这个Node接口在JavaScript中是作为Node类型实现的;除了IE之外,在其他所有浏览器中都可以访问到这个类型。
  • JavaScript中的所有节点类型都继承自Node类型,因此所有节点类型都共享着相同的基本属性和方法。
  • 每个节点都有一个nodeType属性,用于表明节点的类型。及诶单类型由在Node类型中定义的下列12个数值常量来表示,任何节点类型必居其一(编号为节点类型常量存储的数值):

    1. Node.ELEMENT_NODE
    2. Node.ATTRIBUTE_NODE
    3. Node.TEXT_NODE
    4. Node.CDATA_SECTION_NODE
    5. Node.ENTITY_REFERENCE_NODE
    6. Node.ENTITY_NODE
    7. Node.PROCESSING_INSTRUCTION_NODE
    8. Node.COMMENT_NODE
    9. Node.DOCUMENT_NODE
    10. Node.DOCUMENT_TYPE_NODE
    11. Node.DOCUMENT_FRAGMENT_NODE
    12. Node.NOTATION_NODE
// 通过比较上面的常量,很容易的确定节点类型
// 在IE中无效
if (someNode.nodeType == Node.ELEMENT_NODE) {
  console.log("Node is an element");
}

// 由于IE没有公开 Node 类型的构造函数
// 最好还是将 nodeType 属性与数字比较
if (someNode.nodeType == 1) {
  console.log("Node is an element");
}
  • 并不是所有节点类型都受到Web浏览器的支持。开发人员最常用的就是元素和文本节点。

nodeNamenodeValue 属性

  • 了解节点的具体信息,可以使用nodeNamenodeValue 两个属性。这两个属性的值完全取决于节点类型。在使用这两个值以前,最好用上述的代码检查节点的类型。
if (someNode.nodeType == 1) {
  value = someNode.nodeName;            // nodeName的值是元素的标签名
}

节点关系

  • 每个节点都有一个childNodes属性,其中保存着一个NodeList对象。注意,可以通过方括号语法来访问NodeList的值,而且也有length属性,但它并不是Array的实例
  • NodeList对象的独特之处在于,它实际上是基于DOM结构动态执行查询的结果,因此DOM结构的变化能够自动反映在NodeList对象中。我们常说NodeList是有生命、有呼吸的对象,而不是我们第一次访它的瞬间拍摄下来的一张快照。
// 方括号和 item() 语法结果是相同的
var firstChild = someNode.childNodes[0];
var secondChild = someNode.childNodes.item(1);
var count = comeNode.childNodes.length;

// 虽然不是Array的实例,但我们可以将它转换成数组
// 在IE8及之前的版本中无效
var arrayOfNodes = Array.prototype.slice.call(someNode.childNodes, 0);
// 由于IE8及更早版本将 NodeList 实现为一个 COM 对象
// 必须手动枚举所有成员,才能转换成数组
function convertToArray(nodes) {
  var array = null;
  try {
    array = Array.prototype.slice.call(nodes, 0);      // 针对非IE浏览器
  } catch (ex) {
    array = new Array();
    for (var i=0, len=nodes.length; i < len; i++) {
      array.push(nodes[i]);
    }
  }

  return array;
}
  • 每个节点都有一个parentNode属性,指向文档中的父节点。
  • 包含在childNodes中的所有节点都具有相同的父节点,而相互之间是同胞节点。
  • 通过每个节点的previousSiblingnextSibling 属性可以访问同一列表中的其他节点。列表第一个节点previousSiblingnull,列表最后一个nextSiblingnull,当然如果列表只有一个节点,那么两个都是null
  • 父节点的firstChildlastChild属性分别指向第一个和最后一个。如果列表没有节点,那么两个属性都是null

图片描述

  • hasChildNodes()也是一个非常有用的方法,当查询节点存在子节点时返回true,不存在返回false。这是比查询childNodes.length更简单的方法。
  • 所有节点都有的最后一个属性是ownerDocument,该属性指向表示整个文档的文档节点。这种关系表示的是任何节点都属于它所在的文档,任何节点都不能同时存在两个或更多个文档中。通过这个属性,我们可以不必在节点层次中通过层层回溯达到顶端,而是可以直接访问文档节点。

操作节点

  • 因为关系指针都是只读的,所以DOM提供了一些操作节点的方法 。
  • 最常用的方法是appendChild(),用于向childNodes列表的末尾添加一个节点,执行后,方法返回新增的节点。
var returnedNode = someNode.appendChild(newNode);
console.log(returnedNode == newNode);           // true
console.log(someNode.lastChild ==newNode);      // true
  • 如果需要把节点放在childNodes列表中某个特定的位置上,而不是放在末尾,可以使用insertBefore()方法。这个方法接收两个参数:要插入的节点和作为参照的节点。插入节点后,被插入的节点会变成参照节点的前一个同胞节点(previousSibling),同时被方法返回。如果参照节点是null,则 insertBefore()appendChild() 执行相同操作。
// 插入后成为最后一个子节点
var returnedNode = someNode.insertBefore(newNode, null);

// 插入后成为第一个子节点
var returnedNode = someNode.insertBefore(newNode, someNode.firstChild);

// 插入后在最后一个子节点前面
var returnedNode = someNode.insertBefore(newNode, someNode.lastChild);
  • replaceChild() 替换节点。同样接收两个参数,插入的节点和参照节点。插入新的节点并将参照节点从文档树中移除,新的节点会从被替换的节点复制所有关系指针。尽管从技术上讲,被替换的节点仍然在文档中,但它在文档中的位置已经不存在了。
  • removeChild() 移除节点。被移除的节点仍然在文档中,但它在文档中的位置已经不存在了。
  • 以上四个方法必须先取得操作节点的父节点(代码示例中是someNode)。在不存在子节点的节点上调用以上方法,会导致错误。

其他方法

  • 还有两个方法是所有节点都有的。
  • cloneNode() 用于创建调用这个方法的节点的一个完全相同的副本。接收一个布尔值参数,表示是否执行深复制。

    • 传入true。执行深复制,复制节点及其整个子节点树
    • 传入false。执行浅复制,即只复制节点本身。
    • 复制返回的节点副本属于文档所有,但并没有为它制定父节点。因此这个节点副本就成为了一个“孤儿”,除非通过 appendChild() insertBefore() replaceChild() 将它添加到文档中。
    • IE8及之前的版本不会为包含空白符的文字创建节点(TEXT)
    • clone() 方法不会复制添加到DOM节点中的JavaScript属性,例如时间处理程序。这个方法只复制特性、(在明确指定的情况下也复制)子节点,其他一切都不会复制。
    • IE 会复制事件处理程序,所以我们建议在复制之前最好先移出事件处理程序。
<ul id="ul">
  <li>item 1</li>
  <li>item 2</li>
  <li>item 3</li>
</ul>
var myList = document.getElementById("ul");
var deepList = myList.cloneNode(true);
// [text, li, text, li, text, li, text]
console.log(deepList.childNodes);
// 3 (IE < 9) 或 7 (其他浏览器)
// IE8及之前的版本不会为包含空白符的文字创建节点(TEXT)
console.log(deepList.childNodes.length);

var shallowList = myList.cloneNode(false);
console.log(shallowList.childNodes.length);     // 0
  • normalize() 方法唯一的作用就是处理文档树中的文本节点。由于解析器的实现或DOM操作等原因,可能会出现文本节点不包含文本,或者连接出现两个节点的情况。当在某个节点上调用这个方法时,就会在该节点的后代节点中查找上述两种情况。

    • 如果找到了空文本节点,则删除它
    • 如果找到相邻的文本节点,则将它们合并为一个文本节点
    • 本章后面还将进一步讨论方法
var html = document.documentElement;               // 取得对<html>的引用
console.log(html == document.childNodes[0]);       // true
console.log(html == document.firstchild)           // true
  • 所有浏览器都支持document.documentElementdocument.boyd 属性
  • Document另一个可能的子节点是DocumentType。通常将<!DOCTYPE>标签看成一个与文档其他部分不同的实体,可以通过doctype属性(在浏览器中是document.doctype)来访问信息。
  • 浏览器对document.doctype的支持差别很大,所以这个属性的用途很有限:

    • IE8及之前版本,如果存在文档类型声明,会将其错误的解释为一个注释并把它当做Comment节点;而document.doctype的值始终为null
    • IE9+,如果存在文档类型声明,则将其作为文档的第一个子节点;document.doctype是一个DocumentType节点,也可以通过document.firstChilddocument.childNodes[0]访问同一个节点。
    • Safari, Chrome, Opera :如果存在文档类型声明,则将其解析,但不作为文档的子节点。document.doctype是一个DocumentType节点,但该节点不会出现在document.childNodes中。
  • 从技术上说,出现在<html>元素外部的注释应该是算是文档的子节点。然而,不同的浏览器在是否解析这些注释以及能否正确处理他们等方面,也存在很大差异。
<!-- 第一条注释 -->
<html>
  <body>
  </body>
</html>
<!-- 第二条注释 -->
  • 看起来这个页面应该有3个子节点:注释、<html>元素、注释。从逻辑上讲,我们会认为document.childNodes中应该包含与这3个节点对应的3项。但是实际上,浏览器存在以下差异:

    • IE8及之前版本、Safari3.1及更高版本、Opera和Chrome 只为第一条注释创建节点,不为第二条注释创建节点。结果第一条注释就会成为document.childNodes中的第一个子节点。
    • IE9+,将会将两条都创建节点。
    • Firefox 和 Safari3.1之前的版本会完全忽略这两条注释。
  • 多数情况下,我们都用不着在document对象上调用appendChild() removeChild() replaceChild() 方法,因为文档类型(如果存在的话)是只读的,而且它只能有一个元素子节点(该节点通常早就已经存在了)。

文档信息

  • 作为HMLTDocument的一个实例,document对象还有一些标准的Document对象所没有的属性。
  • title 包含着<title>元素中的文本。通过这个属性可以取得当前页面的标题,也可以修改当前页面的标题并反映在浏览器的标题栏中。修改title属性的值会改变<title>元素。
// 取得文档标题
var originalTitle = document.title;

// 设置文档标题
document.title = "New page title";
  • 下面三个属性与网页的请求有关,所有这些信息都存在于请求的HTTP头部,只不过是通过这些属性让我们能够在JavaScript中访问它们而已:

    • URL属性中包含页面完整的URL(地址栏中的URL)
    • domain属性中值包含页面的域名
    • referrer属性中可能会包含空字符串
    • URLdomain属性是相互关联的。例如document.URL等于"http://www.wrox.com/WileyCDA/",那么document.domain就等于"www.wrox.com"。
    • 3个属性中只有domain可以设置,但有安全方面的限制。如果URL中包含一个子域名,例如"p2p.wrox.com",那么就只能讲domain设置为"wrox.com"(URL中包含"www",如"www.wrox.com"时,也是如此)。
    • 当页面中包含来自其他子域的框架或内嵌框架时,能够设置document.domain就非常方便了。由于跨域安全限制,来自不同子域的页面无法通过JavaScript通信。而通过将每个页面的document.domain设置为相同的值,这些页面就可以互相访问对方包含的JavaScript对象了。
// 取得完整的URL
var url = document.URL;

// 取得域名
var domain = document.domain;

// 取得来源 页面的URL
var referrer = document.referrer;
  • 浏览器对domain属性还有一个限制,即如果域名一开始是“松散的”(loose),那么就不能将它再设置为“紧绷的”(tight)。
// 假设页面来自于 p2p.wrox.com域

document.domain = "wrox.com";         // 松散的(成功)
document.domain = "p2p.wrox.com";     // 紧绷的(出错)

查找元素

  • getElementById() 接收一个参数:要取得的元素的ID。找到相应的元素则返回该元素,否则返回null

    • IE8及较低版本不区分ID大小写
    • 如果页面多个元素的ID相同,只会返回第一个匹配的元素。
    • IE7及更早的版本添加了一个怪癖:name特性与给定ID匹配的表单元素也会被该方法返回。
    <input type="text" name="myElement" value="Text field">
    <div id="myElement">A div</div>
    <script>
      // IE7中调用会返回<input>元素
      var el = document.getElementById("myElement");
    </script>
  • getElementsByTagName() 接收一个参数:要取得的元素的标签名,而返回的是包含零或多个元素的NodeList。可以使用方括号语法或item()方法来访问对象中的项。
  • namedItem() 使用这个方法可以通过元素的name特性取得集合中的项。或方括号语法能达到同样的效果
<img src="myimage.gif" name="myImage">

<script>
  var images = document.getElementsByTagName("img");

  console.log(images.length);
  console.log(images[0].src);                           // 方括号传入数值就调用 item()
  console.log(images.item(0).scr);
  var myImage = images.namedItem("myImage");
  var myImage = images["myImage"];                      // 方括号传入字符串就调用namedItem()
</script>
  • 要取得文档中的所有元素,可以向getElementsByTagName()中传入"*"。在JavaScript及CSS中,星号通常表示全部。
  • 虽然标准规定标签名需要区分大小写,但为了最大限度的与既有HTML页面兼容,传给getElementsByTagName()的标签名是不需要区分大小写的。但对于XML页面而言(包括XHTML),getElementsByTagName()方法就会区分大小写。
  • getElementByName() 是只有HTMLDocument类型才有的方法,返回带有给定name属性的所有元素。最常使用的情况是取得单选按钮;为了确保发送给浏览器的值正确无误,所有单选按钮必须具有相同的name特性
<fieldset>
  <legend>Which color do you prefer?</legend>
  <ul>
    <li>
      <input type="radio" value="red" name="color" id="colorRed">
      <label for="colorRed">Red</label>
    </li>
    <li>
      <input type="radio" value="green" name="color" id="colorGreen">
      <label for="colorGreen">Green</label>
    </li>
    <li>
      <input type="radio" value="blue" name="color" id="colorBlue">
      <label for="colorBlue">Blue</label>
    </li>
  </ul>
</fieldset>
  • 上述例子使用getElementsByName()方法可以返回三个input元素。但是对于这里的单选按钮来说namedItem()方法只会取得第一项(因为每一项的name特性都相同)。

特殊集合

  • document.anchors 包含文档中所有带name特性的<a>元素
  • document.applets 包含文档中所有的<form>元素,与document.getElementsByTagName("form")得到的结果相同
  • document.images 包含文档中所有的<img>元素,与document.getElementsByTagName("img")得到的结果相同
  • document.links 包含文档中所有带 href 特性的<a>元素

DOM一致性检测

  • 由于DOM分为多个级别,也包含多个部分,因此检测浏览器实现了DOM的哪些部分就十分必要。document.implementation属性就是为此提供的,与浏览器对DOM的实现直接对应。
  • DOM1级别只为document.implementation规定了一个方法,即hasFeature()。接收两个参数:要检测的DOM功能的名称及版本号。如果支持返回true
var hasXmlDom = docuemnt.implementation.hasFeature("XML", "1.0");
  • 下表列出了可以检测的不同值得版本号
功能 版本号 说明
Core 1.0、2.0、3.0 基本的DOM,用于描述表现文档的节点树
XML 1.0、2.0、3.0 Core的XML拓展,添加了对CDATA、处理指令及实体的支持
HTML 1.0、2.0 XML的HTML拓展,添加了对HTML特有元素及实体的支持
Views 2.0 基于某些样式完成文档的格式化
StyleSheets 2.0 将样式表关联到文档
CSS 2.0 对层叠样式表1级的支持
CSS2 2.0 对层叠样式表2级的支持
Events 2.0, 3.0 常规的DOM事件
UIEvents 2.0, 3.0 用户界面事件
MouseEvents 2.0, 3.0 由鼠标引发的事件(click、mouseover等)
MutationEvents 2.0, 3.0 DOM树变化时引发的事件
HTMLEvents 2.0 HTML4.01事件
Range 2.0 用于操作DOM树种某个范围的对象和方法
Traversal 2.0 遍历DOM树的方法
LS 3.0 文件与DOM树之间的同步加载和保存
LS-Asnyc 3.0 文件与DOM树之间的异步加载和保存
Validation 3.0 在确保有效的前提下修改DOM树的方法
  • hasFeature() 方法确实方便,但也有缺点。因为实现者可以自行决定是否与DOM规范的不同部分保持一致。事实上,想让hasFearture()针对所有值都有返回true很容易,但返回true有时候也不意味着实现与规范一致。
  • 为此我们建议,在使用hasFreatrue()之外,还同时使用能力检测。

文档写入

  • write()writeln()方法都接收一个字符串参数,即要写入到输出流中的文本。wirte()会原样写入,而writeln()则会在字符串的末尾添加一个换行符(n)。在页面加载的过程中,可以使用这两个方法动态的加入内容。
  • 在包含JavaScript文件时,必须注意不能像下面的例子那样直接包含字符串"</script>",因为这会导致该字符串被解释为脚本块的结束,后面的代码将无法执行。使用转义"</script>"可以避免这个问题。
  • open()close()分别用于打开和关闭网页的输出流。如果是在页面加载期间使用write()writeln()方法,则不需要用到这两个方法。
  • 严格型XHTML文档不支持文档吸入。对于那些按照application/xml+xhtml内容类型提供的页面,这两个方法也同样无效。

Element类型

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

    • nodeType的值为1
    • nodeName的值为元素的标签名
    • nodeValue的值为null
    • parentNode的值可能为Dcoment或Element
    • 其子节点可能是 ElementTextCommentProcessingInstructionCDATASectionEntityReference
  • 访问元素的标签名,可以使用nodeName属性,也可以是使用tagName属性,这两个属性会返回相同的值。
var div = document.getElementById("myDiv");
console.log(div.tagName);                   // "DIV"
console.log(div.nodeName);                  // "DIV"
console.log(div.tagName == div.nodeName);   // true
if (element.tagName == "div") {
  // 不能这样比较,很容易出错
}

if (element.tagName.toLowerCase() == "div") {
  // 推荐这样做(适用于任何文档)
}

HTML元素

  • 所有HTML元素都由HTMLElement类型表示。HTMLElement类型直接继承自Elment并添加了一些属性。每个HTML元素中都存在的下列标准特性:

    • id 元素在文档中的唯一标识符
    • title 有关元素的附加说明信息,一般通过工具提示条显示出来
    • lang 元素内容的语言代码,很少使用
    • dir 语言的方向值为"ltr"(left-to-right 从左至右)或 "rtl"
    • className 与元素的class特性对应,即为元素指定的CSS类。没有将这个属性命名为class是因为class是ECMAScript的保留字。
  • 并不是对所有属性的修改都会在页面中直观的表现出来。对id或lang的修改对用户而言是透明不可见的。而对title的修改则只会在鼠标移动到这个元素之上时才会显示出来。对dir的修改会在属性重写的那一刻,立即影响页面中文本的左右对齐方式。修改className时,如果新类关联了与此前不同的CSS样式,就立即应用新的样式。
  • 下面表格列出了所有HTML元素以及与之关联的类型(以斜体印刷的元素表示不推荐使用了)。注意表中的这些类型在Opera、Safari、Chrome、Firefox中都可以通过JavaScript访问,但在IE8之前的版本中,不能通过JavaScript访问。

图片描述
图片描述

取得特性

  • 操作特性的DOM方法主要有三个,分别是getAttribute()setAttribute()removeAttribute()
var div = document.getElemntByid("myDiv");
console.log(div.getAttribute("id"));          // "myDiv"
console.log(div.getAttribute("class"));          // "bd"
console.log(div.getAttribute("title"));          // "Body Text"
console.log(div.getAttribute("lang"));          // "en"
console.log(div.getAttribute("dir"));          // "ltr"
  • 注意,传递给getAttribute()的特性名与实际的特性名相同。因此想要得到class特性值,应该传入"class" 而不是"className",后者只在通过对象属性访问特性时才用。
  • 如果给定的特性不存在,getAttribute()返回null
  • 也可以取得自定义特性,即标准HTML语言中没有的特性的值。需要注意,特性的名称不区分大小写,即"ID" 和 "id" 代表的都是同一个特性。另外也要注意,根据HTML5规范,自定义特性应该加上data-前缀以便验证。
  • 任何元素的所有特性,也都可以通过DOM元素本身的属性来访问。当然HTMLElement也会有5个属性与相应的特性一一对应。不过只有公认的(非自定义)特性才会以属性的形式添加到DOM对象中。例如可以通过div.id访问div元素的id属性。不过自定义特性在Safari、Opera、Chrome、Firefox中是不存在的,但IE却会为自定义特性也创建属性。
  • CSS通过getAttribute()访问时,返回的style特性值中包含的是CSS文本,而通过属性来访问它则会返回一个对象。由于style属性是用于以编程方式访问元素样式的(本章后面讨论),因此并没有直接映射到style特性。
  • 时间处理程序(例如onclick)通过getAttribute()访问,返回的是相应的代码字符串。而在访问onclick属性时,则返回的是一个JavaScript函数(如果未在元素中指定相应特性,则返回null)。这是因为onclick及其他事件程序属性本身就应该被赋予函数值。
  • 由于存在上述差别,在通过JavaScript以编程方式操作DOM时,开发人员不经常使用 getAttribute()方法,而只是使用对象的属性。只有在取得自定义特性值得情况下,才会使用getAttribute()方法。
  • 在IE7及以前版本中,通过getAttribute()访问style特性或onclick,返回的值与属性相同,都返回对象值或函数值。虽然IE8已经修复了这个bug,但不同IE版本间的不一致性,也是导致开发人员不适用getAttribute()访问HTML特性的一个原因。

设置特性

  • getAttribute()对应的方法时setAttribute()这个方法接收两个参数:要设置的特性名和值。如果特性已经存在,setAttribute()会以指定的值替换现有的值;如果特性不存在,则创建该属性并设置相应的值。
  • setAttribute()方法既可以操作HTML特性也可以操作自定义特性。通过这个方法设置的特性名会统一转换为小写形式,即"ID"最终变成"id"。
div.setAttribute("id", "someOtherId");
div.id = "someOtherId";

// 添加自定义属性,该属性不会自动成为元素的特性
div.mycolor = "red";
div.getAttribute("mycolor");   // null ie除外
  • removeAttribute() 用于彻底删除元素的特性,调用这个方法不仅会清楚特性的值,而且也会从元素中完全删除特性。这个方法并不常用,IE6及以前版本不支持。
div.removeAttribute("class");

attributes属性

  • Element 类型是使用 attributes 属性的唯一一个DOM节点类型 。
  • attributes属性中包含一个NamedNodeMap,与NodeList类似,也是一个动态集合。元素的每一个 特性都由一个Attr节点表示,每个节点都保存在NamedNodeMap对象中。
  • NamedNodeMap对象拥有以下方法

    • getNamedItem(name):返回nodeName属性等于name的节点
    • removeNamedItem(name):从列表中移除nodeName属性 等于name的节点
    • setNameItem(node):向列表中添加节点,以节点的nodeName属性为索引
    • item(pos):返回位于数字pos位置处的节点
  • attributes属性中包含一系列节点,每个节点的nodeName就是特性的名称,而节点的nodeValue就是特性的值。
// 取得元素的id
var id = element.attributes.getNamedItem("id").nodeValue;

// 设置元素的id
element.attributes["id"].nodeValue = "someOtherId";

// 删除元素id,并返回被删除特性的Attr节点
var oldAttr = element.attributes.removeNamedItem("id");

// 传入一个新的特性节点
element.attributes.setNameItem(newAttr);
  • 由于attributes的方法不够方便,因此开啊人员更多的会使用getAttribute()removeAttribute()setAttribute()方法。如果想要遍历元素特性,可以用attributes
  • 针对attributes对象中的特性,不同浏览器返回的顺序不同。
  • IE7及更早版本返回HTML元素中所有可能的特性,包括没有指定的特性。返回100多个特性是常见的
// 迭代元素的每一个特性,然后构造成 name="value"字符串
function outputAttributes(element) {
  var pairs = new Array(),
      attrName,
      attrValue,
      i,
      len;
  for (i=0, len=elment.attributes.length; i < len; i++) {
    attrName = element.attributes[i].nodeName;
    attrValue = element.attributes[i].nodeValue;
    // 针对 IE7- 做兼容
    // 根据specified属性,只返回指定的特性
    if (element.attributes[i].specified) {
      paris.push(attrName + "=\"" + attrValue + "\"");
    }
  }
  return pairs.join(" ");
}

创建元素

  • document.createElement()方法可以创建新元素。只接收一个参数,即要创建元素的标签名,在HTML文档中不区分大小写,而在XML(包括XHTML)文档中,则是区分大小写。
  • document.createElement()创建元素的同时,也为新元素设置了ownerDcoument属性。此时还可以操作元素的特性,为它添加更多子节点。
  • 由于新元素尚未被添加到文档树中,因此设置这些特性不会影响浏览器的显示。要把新元素添加到文档树,可以使用appendChild() insertBefore() replaceChild()方法。
// 创建
var div = document.createElement("div");
// 操作元素特性,添加子节点
div.id = "myNewDiv";
div.className = "box";
document.body.appendChild(div);
  • 在IE中可以传入完整的元素标签,也可以包含属性(仅IE支持)。这样有助于避开在IE7及更早版本中动态创建元素的某些问题:

    • 不能设置动态创建的<iframe>元素的name特性
    • 不能通过表单的reset()方法重设动态创建的<input>元素(第13章讨论reset()方法)
    • 动态创建的type特性值为“reset”的<button>元素重设不了表单
    • 动态创建的一批name相同的单选按钮彼此毫无关系。
if (client.browser.id && client.browser.ie <= 7) {
  var div = document.createElement("<div id=\"myNewDiv\" class=\"box\"></div>");
}

元素的子节点

  • 元素可以有任意书目的子节点和后台节点,因为元素可以是其他元素的子节点。元素的childNodes属性中包含了它所有子节点,这些子节点可能是元素、文本节点、注释或处理指令。不用浏览器在看待这些节点方面存在显著的不同。
<ul id="myList">
  <li>item 1</li>
  <li>item 2</li>
  <li>item 3</li>
</ul>
  • IE解析,<ul>元素会有3个子节点,分别是3个<li>元素。但如果是其他浏览器,<ul>元素都会有7个元素,包括3个<li>元素和4个文本节点(表示<li>元素之间的空白符)。
  • 如果将元素间的空白符删除,那么所有浏览器都会返回相同数目的子节点
<ul id="myList"><li>item 1</li><li>item 2</li><li>item 3</li></ul>
  • 如果需要通过childNodes属性遍历子节点,那么一定不要忘记浏览器间的这一差别。这意味着在执行某项操作以前,通常都要先检查nodeType属性
for (var i=0, len = element.childNodes.length; i < len; i++) {
  if (element.childNodes[i].nodeTpe == 1) {
    ...
  }
}
  • 如果想通过某个特性的标签名取得子节点或后代节点,可以通过元素调用getElementsByTagName()方法,结果只会返回当前元素的后代。
var ul = document.getElementById("myList");
var items = ul.getElementsByTagName("li");

Text类型

  • 文本节点由Text类型表示,包含的是可以照字面量解释的纯文本内容。纯文本中可以包含转义后的HTML字符,但不能包含HTML代码。
  • Text节点具有以下特征:

    • nodeType的值为3
    • nodeName的值为'#text'
    • nodeValue的值为节点所包含的文本
    • parentNode是一个Element
    • 不支持(没有)子节点
  • 可以通过nodeValue属性或data属性访问Text节点中包含的文本,这两个属性的值相同。对nodeValue的修改也会通过data反映出来,反之亦然。
  • 使用下列方法可以操作节点中的文本

    • appendData(text):将text添加到节点的末尾
    • deleteData(offset, count):从offset指定的位置插入text
    • insertData(offset, text):在offset指定的位置插入text
    • replaceData(offset, count, text):用text替换从offset指定的位置开始到 offset+count为止处的文本
    • splitText(offset):从offset指定的位置将当前文本节点分成两个文本节点。
    • substringData(offset, count):提取从offset指定的位置开始到 offset+count为止处的字符串
    • length属性:保存着节点中字符的书目。而且nodeValue.lengthdata.length中也保存着同样的数值
  • 在默认情况下,每个可以包含内容的元素最多只能有一个文本节点,而且必须确实有内容存在
<!-- 没有内容,也就没有文本节点 -->
<div></div>

<!-- 有空格,因为有一个文本节点 -->
<div> </div>

<!-- 有内容,因为有一个文本节点 -->
<div>Hello World!</div>
// 可以像这样取得文本子节点
var textNode= div.firstChild;   // 或者 div.childNodes[0]

// 取得文本节点的引用后,就可以修改它了
div.firstChild.nodeValue = "Some other message";
  • 如果这个文本节点当前存在于文档树中,那么修改文本节点的结果就会立即得到反映。
  • 修改文本节点时,字符串会经过HTML(或XML,取决于文档类型)编码。换言之,小于号、大于号或引号都会像下面的例子一样被转义
div.firstChild.nodeValue = "Some <strong>other</strong> message";
// 输出结果:"Some &lt;strong&gt;other&lt;/strong&gt; message"
  • 这是在向DOM文档中插入文本之前,先对其进行HTML编码的一种有效方式

创建文本节点

  • document.createTextNode()创建新的文本节点。与设置已有文本节点的值一样,作为参数的文本也将按照HTML或XML的格式进行编码。
var textNode = document.createTextNode("<strong>Hello</strong> World!");
  • 在创建新文本节点的同时,也会为其设置ownerDocument属性。不过除非把新节点添加到文档树中已经存在的节点中,否则我们不会在浏览器窗口中看到新节点。
var element = document.createElement("div");
elment.className = "message";

var textNode = document.createTextNode("Hello world!");
element.appendChild(textNode);

document.body.appendChild(element);
  • 一般情况下,每个元素只有一个文本子节点。不过在某些情况下也可能包含多个文字子节点。相邻的同胞文本节点,之间会连起来,中间不会有空格。
var element = document.createElement("div");
elment.className = "message";

var textNode = document.createTextNode("Hello world!");
element.appendChild(textNode);

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

document.body.appendChild(element);

规范化文本节点

  • DOM文档中存在相邻的同胞文本节点很容易导致混乱,因为分不清文本节点之间的界限。于是催生了一个能够将相邻文本节点合并的方法。
  • normalize()方法是由Node类型定义的(因而在所有节点类型中都存在)。如果在一个包含多个文本节点的父元素上调用normalize()方法,则会将所有文本节点合并成一个文本节点。
var element = document.createElement("div");
elment.className = "message";

var textNode = document.createTextNode("Hello world!");
element.appendChild(textNode);

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

document.body.appendChild(element);

console.log(element.childNodes.length);   // 2

element.normalize();
console.log(element.childNodes.length);   // 1
console.log(element.firstChild.nodeValue); // "Hello World!Yippee!"
  • 浏览器在解析文档时永远不会创建相邻的文本节点,这种情况只会作为DOM操作的结果出现。
  • normalize()有时候会导致IE6崩溃,IE7以上修复了此问题。

分割文本节点

  • splitText()方法会将一个文本节点分割成两个。
var element = document.createElement("div");
elment.className = "message";

var textNode = document.createTextNode("Hello world!");
element.appendChild(textNode);

document.body.appendChild(element);

var newNode = element.firstChild.splitText(5);
console.log(element.firstChild.nodeValue);                   // "Hello"
console.log(newNode.nodeValue);                              // " World!"
console.log(element.childNodes.length);                      // 2

Comment类型

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

    • nodeType的值为8
    • nodeName的值为 "#comment"
    • nodeValue的值是注释的内容
    • parentNode可能是Dcoment或Element
    • 不支持(没有)子节点
  • Comment类型与Text类型继承自相同的基类,因此它拥有除splitText()之外的所有字符串操作方法。
<div id="myDiv"><!--A comment--></div>
var div = document.getElementById("myDiv");
var comment = div.firstChild;
console.log(comment.data);                     // "A comment"
  • 使用document.createComment()并为其传递注释文本也可以创建注释节点
var comment = document.createComment("A comment ");
  • 开发人员很少会创建和访问注释节点,此外浏览器也不会识别位于</html>标签后的注释。如果要访问注释节点,一定要保证它们是位于<html></html>之间。

CDATASection类型

  • CDATASection类型只针对基于XML的文档,表示的是CDATA区域。与Comment类似、CDATASection类型继承自Text类型,因此拥有除splitText()之外的所有字符串操作方法。
  • CDATASection节点具有下列特征:

    • nodeType的值为4
    • nodeName的值为"#cdata-section"
    • nodeValue的值是CDATA区域中的内容
    • parentNode可能是DocumentElement
    • 不支持(没有)子节点
  • CDATA区域只会出现在XML文档中,因此多数浏览器都会把CDATA区域错误的解析为Comment或Element。
<div id="myDiv"><![CDATA[This is some content.]]></div>
  • 这个例子中div元素应该包含一个CDATASection节点。但四大主流浏览器都不能正确解析。即使对于有效的XHTML页面,浏览器也没有正确的支持嵌入的CDATA区域。
  • 在真正的XML文档中,可以使用document.createCDataSection()来创建CDATA区域。

DocumentType类型

  • DocumentType类型在Web浏览器中并不常用,仅有 Firefox Safari 和 Opera支持它。

    • nodeType的值为10
    • nodeName的值为doctype的名称
    • nodeValue的值是null
    • parentNodeDocument
    • 不支持(没有)子节点
  • 通常,浏览器中的文档使用的都是HTML或XHTML文档类型,只有name属性是有用的。
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
console.log(document.doctype.name);          // "HTML"
  • IE不支持DocumentType,因此 document.doctype的值始终都是null

DocumentFragment 类型

  • 所有节点类型中,只有DocumentFragment在文档中没有对应的标记。
  • DOM规定文档片段(document fragment)是一种轻量级的文档,可以包含和控制节点,但不会像完整的文档那样占用额外的资源。

    • nodeType的值为11
    • nodeName的值为"#document-fragment"
    • nodeValue的值是null
    • parentNodenull
    • 子节点可以是ElementProcessingInstructionCommentTextCDATASectionEntityReference
  • 虽然不能把文档文段直接添加到文档中,但可以将它作为一个仓库来使用,在里面保存将来可能会添加到文档中的节点。
  • document.createDocumentFragment() 方法创建文档片段
<ul id="myList"></ul>
var fragment = document.createDocumentFragment();
var ul = document.getElementById("myList");
var li = null;

// 如果直接向ul添加li元素会导致浏览器反复渲染
// fragment作为一个元素中转的仓库避免了这个问题
for (var i=0; i < 3; i++) {
  li = document.createElement("li");
  li.appendChild(document.createTextNode("Item " + (i+1)));
  fragment.appendChild(li);
}

// 这里只会将fragment的所有子节点添加到ul上
// 而fragment本身永远不会成为文档树的一部分
ul.appendChild(fragment);

Attr类型

  • 元素的特性在DOM中以Attr类型来表示。在所有浏览器中(包括IE8),都可以访问 Attr类型的构造函数和原型。

    • nodeType的值为2
    • nodeName的值就是特性的名称
    • nodeValue的值就是特性的值
    • parentNodenull
    • HTML中不支持(没有)子节点
    • XML中子节点可以是Text或EntityReference
  • 尽管Attr是节点,但特性却不被认为是DOM文档树的一部分。
  • Attr对象有三个属性:name value specified
  • document.createAttribute()传入特性的名称可以创建新的特性节点。
var attr = document.createAttribute("align");
attr.value = "left";
element.setAttribute(attr);
console.log(element.attributes["align"].value);      // left
console.log(element.getAttributeNode("align").value);      // left
console.log(element.getAttribute("align"));           // left

DOM操作技术

动态脚本

  • 使用<script>元素可以向页面中插入JavaScript代码,一种是通过其src特性包含外部文件,另一种就是用这个元素本身包含代码。
  • 动态加载的JavaScript文件能够立即运行。
// 在执行最后一行代码把<script>元素添加到页面中之前
// 是不会下载外部文件的
var script = document.createElement("script");
script.type = "text/javascript";
script.src = "client.js";
document.body.appendChild(script);
<script type="text/javascript" src="client.js"></script>
  • 遗憾的是,并没有什么标准方式来探知脚本是否加载完成。
  • 从逻辑上讲,使用行内方式直接插入代码是有效的。在Firefox Safari Chrome Opera中,都可以正常运行,但在IE中,会导致错误。IE将<script>视为一个特殊元素,不允许DOM访问其子节点。不过可以使用<script>元素的text属性来制定JavaScript代码
var script = document.createElement("script");
script.type = "text/javascript";

// 这样IE不支持
script.appendChild(
  document.createTextNode("function sayHi() { console.log('Hi')}")
);
// 可以使用`<script>`元素的text属性来制定JavaScript代码
script.text = "function sayHi() { console.log('Hi')}";

document.body.appendChild(script);
  • Safari3之前的版本不支持这种写法,可以这样做兼容处理
var script = document.createElement("script");
script.type = "text/javascript";
var code = "function sayHi() { console.log('Hi')}"

try {
  script.appendChild(document.createTextNode(code));
} catch (ex) {
  script.text = code;
}

document.body.appendChild(script);
  • 实际上,这样执行代码与在全局作用域中把相同的字符串传递给eval()是一样的。

动态样式

  • 与动态脚本类似,所谓动态样式是指在页面刚加载时不存在的样式;动态样式是在页面加载完成后动态添加到页面中的。
var link = document.createElement("link");
link.rel = "stylesheet";
link.type = "text/css";
link.href = "style.css";
var head = document.getElementsByTagName("head")[0];
head.appendChild(head);
<link rel="stylesheet" type="text/css" href="styles.css">
  • 必须将<link>元素添加到<head>而不是<body>元素,才能保证所在浏览器中的行为一致。
  • 加载外部样式文件的过程是异步的,也就是加载样式 与执行JavaScript代码的过程没有固定的次序。
  • 一般是否知道样式已加载完成并不重要,但也存在几种利用事件来检测这个过程是否完成的技术,将在第13章讨论。
  • 行内方式插入样式也是可以的,同样要对IE做兼容处理
function loadStyleString(css) {
  var style = document.createElement("style");
  style.type = "text/css";
  try {
    style.appendChild(document.createTextNode(css));
  } catch (ex) {
    style.styleSheet.cssText = css;
  }
  document.getElementsByTagName("head")[0].appendChild(style);
}
  • 如果专门针对IE编写代码,务必小心使用styleSheet.cssText属性。在重用同一个<style>元素并再次设置这个属性时,有可能导致浏览器崩溃。同样将cssText属性设置为空字符串也可能导致浏览器崩溃。

操作表格

  • <table>元素是HTML中最复杂的结构之一。想要创建表格,一般都必须涉及表示表格行、单元格、表头等方面。由于涉及的标签多,因而使用核心DOM方法创建和修改表格往往都免不了要编写大量的代码。
<table border="1" width="100%">
  <tbody>
    <tr>
      <td>Cell 1,1</td>
      <td>Cell 2,1</td>
    </tr>
    <tr>
      <td>Cell 1,2</td>
      <td>Cell 2,2</td>
    </tr>
  </tbody>
</table>
// 使用核心DOM方法创建这些元素
// 创建table
var table = document.createElement("table");
table.border = 1;
table.width = "100%";

// 创建tbody
var tbody = document.createElement("tbody");
table.appendChild(tbody);

// 创建第一行
var row1 = document.createElement("tr");
tbody.appendChild(row1);
var cell1_1 = document.createElement("td");
cell1_1.appendChild(document.createTextNode("Cell 1,1"));
row1.appendChild(cell1_1);
var cell2_1 = document.createElement("td");
cell2_1.appendChild(document.createTextNode("Cell 2,1"));
row1.appendChild(cell2_1);

// 创建第二行
var row2 = document.createElement("tr");
tbody.appendChild(row2);
var cell1_2 = document.createElement("td");
cell1_2.appendChild(document.createTextNode("Cell 1,2"));
row2.appendChild(cell1_2);
var cell2_2 = document.createElement("td");
cell2_2.appendChild(document.createTextNode("Cell 2,2"));
row2.appendChild(cell2_2);

// 将表格添加到文档主体中
document.body.appendChild(table);
  • DOM代码很长,还有点不好理解。为了方便构建表格,HTMLDOM还为<table> <tbody> <tr> 元素添加了一些属性和方法。
  • <table>元素添加的属性和方法:

    • caption: 保存着对<caption>元素(如果有)的指针
    • tBodies: 是一个<tbody>元素的HTMLCollction
    • tFoot: 保存着对<tfoot>元素的(如果有)指针
    • tHead: 保存着对<thead>元素的(如果有)指针
    • rows: 是一个表格中所有行的HTMLCollection
    • createTHead(): 创建<thead>元素,将其放到表格中,返回引用
    • createTFoot(): 创建<tfoot>元素,将其放到表格中,返回引用
    • createCaption(): 创建<caption>元素,将其放到表格中,返回引用
    • deleteTHead(): 删除<thead>元素
    • deleteTFoot(): 删除<tfoot>元素
    • deleteCaption(): 删除<caption>元素
    • deleteRow(pos): 删除指定位置的行
    • insertRow(pos): 向rows集合中的指定位置插入一行
  • <tbody>元素添加的属性和方法如下:

    • rows: 保存着<tbody>元素中行的HTMLCollection
    • deleteRow(pos): 删除指定位置的行
    • insertRow(pos): 向rows集合中的指定位置插入一行
  • <tr>元素添加的属性和方法如下:

    • cells: 保存着<tr>元素中单元格的HTMLCollection
    • deleteCell(pos): 删除指定位置的单元格
    • insertCell(pos): 向cells集合中的指定位置插入一个单元格,返回对新插入单元格的引用。
// 根据以上属性和方法,可以大大简化前述代码

// 创建table
var table = document.createElement("table");
table.border = 1;
table.width = "100%";

// 创建tbody
var tbody = document.createElement("tbody");
table.appendChild(tbody);

// 创建第一行
tbody.insertRow(0);
tbody.rows[0].insertCell(0);
tbody.rows[0].cells[0].appendChild(document.createTextNode("Cell 1,1"));
tbody.rows[0].insertCell(1);
tbody.rows[0].cells[1].appendChild(document.createTextNode("Cell 2,1"));

// 创建第二行
tbody.insertRow(0);
tbody.rows[1].insertCell(0);
tbody.rows[1].cells[0].appendChild(document.createTextNode("Cell 1,2"));
tbody.rows[1].insertCell(1);
tbody.rows[1].cells[1].appendChild(document.createTextNode("Cell 2,2"));

// 将表格添加到文档主体中
document.body.appendChild(table);

使用NodeList

  • 理解 NodeList 及其近亲 NamedNodeMapHTMLCollection,是从整体上透彻理解DOM的关键所在。这三个集合都是动态的,每当文档结构发生变化,它们都会得到更新。
  • 本质上说,所有NodeList对象都是在访问DOM文档实时运行的查询。
// 下列代码会导致无限循环
var divs = document.getElementsByTagName("div");
var div;

// 每次循环都要对条件 i < divs.length 求值
// 但每次循环都添加了一个新的div
for (var i=0; i < divs.length; i++) {
  div = document.createElement("div");
  document.body.appendChild(div);
}
// 最好使用length属性初始化第二个变量
var divs = document.getElementsByTagName("div");
var i, len, div;

// len保存着第一次循环时div的数量,不会随着循环增加
for (i=0, len=divs.length; i < len; i++) {
  div = document.createElement("div");
  document.body.appendChild(div);
}
  • 尽量减少访问NodeList的次数,因为每次访问都会运行一次基于文档的查询。可以考虑将从NodeList中取得的值缓存起来。
  • 理解DOM的关键就是理解DOM对性能的影响。DOM操作往往是JavaScript程序中开销最大的部分。有鉴于此,最好减少DOM操作。

maroonstar
11 声望3 粉丝