DOM2和DOM3

本文共--字 阅读约--分钟 | 浏览: -- Last Updated: 2021-07-03

DOM1级主要定义的是HTMLXML文档的底层结构。DOM2DOM3级则在这个结构的基础上引入了更多的交互能力,也支持了更高级的XML特性。

  • DOM2 级核心(DOM Level 2 Core):在1 级核心基础上构建,为节点添加了更多方法和属性。
  • DOM2 级视图(DOM Level 2 Views):为文档定义了基于样式信息的不同视图。
  • DOM2 级事件(DOM Level 2 Events):说明了如何使用事件与DOM文档交互。
  • DOM2 级样式(DOM Level 2 Style):定义了如何以编程方式来访问和改变CSS样式信息。
  • DOM2 级遍历和范围(DOM Level 2 Traversal and Range):引入了遍历DOM文档和选择其特定部分的新接口。
  • DOM2 级 HTML(DOM Level 2 HTML):在1 级HTML基础上构建,添加了更多属性、方法和新接口。

一、DOM 变化

针对XML命名空间的变化

有了XML命名空间,不同XML文档的元素就可以混合在一起,共同构成格式良好的文档,而不必担心发生命名冲突。从技术上说,HTML不支持XML命名空间,但XHTML支持XML命名空间。因此,本节给出的都是XHTML的示例。

命名空间要使用xmlns特性来指定。XHTML的命名空间是http://www.w3.org/1999/xhtml,在任格式良好XHTML页面中,都应该将其包含在<html>元素中;要想明确地为XML命名空间创建前缀,可以使用xmlns后跟冒号,之后为前缀;有时候为了避免不同语言间的冲突也可以使用命名空间来限定特性,比如下例中的class特性

<xhtml:html xmlns:xhtml="http://www.w3.org/1999/xhtml">
  <xhtml:head>
    <xhtml:title>Example XHTML page</xhtml:title>
  </xhtml:head>
  <xhtml:body xhtml:class="home">
    Hello world!
  </xhtml:body>
</xhtml:html>

在只基于一种语言编写XML文档的情况下,命名空间实际上也没有什么用。不过,在混合使用两种语言的情况下,命名空间的用处就非常大了。混合了XHTMLSVG语言的文档:

<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
      <title>Example XHTML page</title>
  </head>
  <body>
    <svg xmlns="http://www.w3.org/2000/svg" version="1.1"
      viewBox="0 0 100 100" style="width:100%; height:100%">
      <rect x="0" y="0" width="100" height="100" style="fill:red"/>
    </svg>
</body>
</html>

在这个例子中,通过设置命名空间,将<svg>标识为了与包含文档无关的元素。此时,<svg>元素的所有子元素,以及这些元素的所有特性,都被认为属于http://www.w3.org/2000/svg命名空间。即使这个文档从技术上说是一个XHTML文档,但因为有了命名空间,其中的SVG代码也仍然是有效的。

1、 Node 类型的变化

DOM2级中,Node类型包含下列特定于命名空间的属性。

  • localName:不带命名空间前缀的节点名称。
  • namespaceURI:命名空间URI或null
  • prefix:命名空间前缀或null

下例中,对于<html>元素来说,它的localNametagName"html"namespaceURI"http://www.w3.org/1999/xhtml",而prefixnull。对于<s:svg>元素而言,它的localName"svg"tagName"s:svg"namespaceURI"http://www.w3.org/2000/svg",而prefix"s"

<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <title>Example XHTML page</title>
  </head>
  <body>
    <s:svg xmlns:s="http://www.w3.org/2000/svg" version="1.1"
      viewBox="0 0 100 100" style="width:100%; height:100%">
      <s:rect x="0" y="0" width="100" height="100" style="fill:red"/>
    </s:svg>
  </body>
</html>

DOM3级在此基础上更进一步,又引入了下列与命名空间有关的方法。

  • isDefaultNamespace(namespaceURI):判断指定的namespaceURI是否是当前节点的默认命名空间,返回布尔值
  • lookupNamespaceURI(prefix):返回给定prefix的命名空间(namespaceURI)。
  • lookupPrefix(namespaceURI):返回给定namespaceURI的前缀(prefix)。

2、Document 类型的变化

  • createElementNS(namespaceURI, tagName):使用给定的tagName创建一个属于命名空间namespaceURI的新元素。
  • createAttributeNS(namespaceURI, attributeName):使用给定的attributeName创建一个属于命名空间namespaceURI的新特性。
  • getElementsByTagNameNS(namespaceURI, tagName):返回属于命名空间namespaceURItagName元素的NodeList

3、Element 类型的变化

  • getAttributeNS(namespaceURI,localName):取得属于命名空间namespaceURI且名为localName的特性。
  • getAttributeNodeNS(namespaceURI,localName):取得属于命名空间namespaceURI且名为localName 的特性节点。
  • getElementsByTagNameNS(namespaceURI, tagName):返回属于命名空间namespaceURItagName元素的NodeList
  • hasAttributeNS(namespaceURI,localName):确定当前元素是否有一个名为localName的特性,而且该特性的命名空间是namespaceURI。注意,“DOM2 级核心”也增加了一个hasAttribute()方法,用于不考虑命名空间的情况。
  • removeAttriubteNS(namespaceURI,localName):删除属于命名空间namespaceURI且名为localName 的特性。
  • setAttributeNS(namespaceURI,qualifiedName,value):设置属于命名空间namespace-URI且名为qualifiedName的特性的值为value
  • setAttributeNodeNS(attNode):设置属于命名空间namespaceURI的特性节点。

其他方面的变化

1、DocumentType 类型的变化

DocumentType类型新增了3 个属性:publicIdsystemIdinternalSubset(内部子集), 前两个属性表示的是文档类型声明中的两个信息段,这两个信息段在DOM1级中是没有办法访问到的。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"
[<!ELEMENT name (#PCDATA)>] >

publicId"-//W3C//DTD HTML 4.01//EN"
systemId"http://www.w3.org/TR/html4/strict.dtd"
document.doctype.internalSubset"<!ELEMENT name (#PCDATA)>"

2、Document 类型的变化

2.1、 importNode

这个方法的用途是从一个文档中取得一个节点,然后将其导入到另一个文档,使其成为这个文档结构的一部分。需要注意的是,每个节点都有一个ownerDocument属性,表示所属的文档。

如果调用appendChild时传入的节点属于不同的文档,ownerDocument属性的值不一样,则会导致错误。但在调用importNode时传入不同文档的节点则会返回一个新节点,这个新节点的所有权归当前文档所有。

importNode方法与cloneNode方法非常相似,它接受两个参数(要复制的节点、是否复制子节点的布尔值),返回的结果是原来节点的副本,但能够在当前文档中使用。

var newNode = document.importNode(oldNode, true); //导入节点及其所有子节点
document.body.appendChild(newNode);
// 这个方法在HTML文档中并不常用,在XML文档中用得比较多

2.2、defaultView属性

保存着一个指针,指向拥有给定文档的窗口(或框架),除 IE之外的所有浏览器都支持defaultView属性。在IE中有一个等价的属性名叫parentWindow(Opera 也支持这个属性)。因此,要确定文档的归属窗口,可以使用以下代码。

var parentWindow = document.defaultView || document.parentWindow;

2.3、“DOM2 级核心”还为document.implementation对象规定了两个新方法。

  • createDocumentType:用于创建一个新的DocumentType节点,接受3 个参数:文档类型名称、publicIdsystemId

  • createDocument:用于创建新文档,这个方法接受3 个参数:针对文档中元素的namespaceURI、文档元素的标签名、新文档的文档类型。要想创建一个XHTML 文档,可以使用以下代码。

var doctype = document.implementation.createDocumentType(
      "html",
      " -//W3C//DTD XHTML 1.0 Strict//EN",
      "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"
);
var doc = document.implementation.createDocument("http://www.w3.org/1999/xhtml","html", doctype);

// 不过,新文档当前只有文档元素`<html>`,剩下的所有元素都需要继续添加。

2.4、“DOM2 级HTML”模块也为document.implementation 新增了一个方法,名叫createHTMLDocument。这个方法的用途是创建一个完整的HTML文档,是HTMLDocument类型的实例,因而具有该类型的所有属性和方法,包括titlebody属性

var htmldoc = document.implementation.createHTMLDocument("New Doc");
alert(htmldoc.title); //"New Doc"
alert(typeof htmldoc.body); //"object"

3、Node 类型的变化

DOM3级引入了两个辅助比较节点的方法:isSameNode()isEqualNode()。这两个方法都接受一个节点参数,并在传入节点与引用的节点相同或相等时返回true。所谓相同isSameNode(),指的是两个节点引用的是同一个对象。所谓相等isEqualNode(),指的是两个节点是相同的类型,具有相等的属性(nodeNamenodeValue,等等),而且它们的attributeschildNodes属性也相等(相同位置包含相同的值)。

var div1 = document.createElement("div");
div1.setAttribute("class", "box");
var div2 = document.createElement("div");
div2.setAttribute("class", "box");
alert(div1.isSameNode(div1)); //true
alert(div1.isEqualNode(div2)); //true
alert(div1.isSameNode(div2)); //false

4、框架的变化

框架和内嵌框架分别用HTMLFrameElementHTMLIFrameElement表示,它们在DOM2级中都有了一个新属性,名叫contentDocument。这个属性包含一个指针,指向表示框架内容的文档对象。在此之前,无法直接通过元素取得这个文档对象(只能使用frames集合)。IE8 之前不支持框架中的contentDocument属性,但支持一个名叫contentWindow的属性,该属性返回框架的window对象,而这个window对象又有一个document属性

var iframe = document.getElementById("myIframe");
var iframeDoc = iframe.contentDocument || iframe.contentWindow.document;

访问框架或内嵌框架的文档对象要受到跨域安全策略的限制。如果某个框架中的页面来自其他域或不同子域,或者使用了不同的协议,那么要访问这个框架的文档对象就会导致错误。

二、样式

访问元素的样式

任何支持style特性的HTML元素在JavaScript中都有一个对应的style属性。这个style对象是CSSStyleDeclaration的实例,包含着通过HTMLstyle特性(属性)指定的所有样式信息,但不包含与外部样式表或嵌入样式表经层叠而来的样式。

style特性中指定的任何CSS属性都将表现为这个style对象的相应属性。对于使用短划线分隔的词汇,例如background-imageCSS属性名,必须将其转换成驼峰大小写形式,才能通过JavaScript来访问。

多数情况下,都可以通过简单地转换属性名的格式来实现转换。也有不能直接转换的CSS属性就是float。由于floatJavaScript中的保留字,因此不能用作属性名。DOM2级样式 规范规定样式对象上相应的属性名应该是cssFloat,而IE支持的则是styleFloat。还有就是class也是保留字,所以需要使用className来访问。

在以这种方式改变样式时,元素的外观会自动被更新。使用JavaScript操作style对象,都是操作的行内样式,层级较高,不会被覆盖。也可以通过style对象取到相应的特性的值,如果没有为元素设置style特性,那么style对象中可能会包含一些默认的值,但这些值并不能准确地反映该元素的样式信息。

1、DOM 样式属性和方法

  • cssText:通过它能够访问到style特性(行内属性)中的CSS代码。
  • length:应用给元素的CSS属性的数量。
  • parentRule:表示CSS信息的CSSRule对象。本节后面将讨论CSSRule类型。
  • getPropertyPriority(propertyName):如果给定的属性使用了!important 设置,则返回"important";否则,返回空字符串。
  • getPropertyValue(propertyName):返回给定属性的字符串值。
  • item(index):返回给定位置的CSS属性的名称。
  • removeProperty(propertyName):从样式中删除给定属性。
  • setProperty(propertyName,value,priority):将给定属性设置为相应的值,并加上优先权标志("important"或者一个空字符串)。

2、计算样式

2.1、getComputedStyle()方法

这个方法接受两个参数:要取得计算样式的元素和一个伪元素字符串(例如":after")。如果不需要伪元素信息,第二个参数可以是nullgetComputedStyle()方法返回一个CSSStyleDeclaration对象(与style属性的类型相同),其中包含当前元素的所有计算的样式(最终样式)

<!DOCTYPE html>
<html>
  <head>
    <title>Computed Styles Example</title>
    <style type="text/css">
      #myDiv {
        background-color: blue;
        width: 100px;
        height: 200px;
      }
    </style>
  </head>
  <body>
    <div id="myDiv" style="background-color: red; border: 1px solid black"></div>
    <script>
      var myDiv = document.getElementById("myDiv");
      var computedStyle = document.defaultView.getComputedStyle(myDiv, null);
      alert(computedStyle.backgroundColor); // "red"/ "rgb(255,0,0)" 嵌入样式会被行内样式覆盖掉 
      alert(computedStyle.width); // "100px"
      alert(computedStyle.height); // "200px"
      alert(computedStyle.border); // 在某些浏览器中是"1px solid black" / "1px solid rgb(0,0,0)"
    </script>
  </body>
</html>

在设置border 时, 实际上是设置了四个边的边框宽度、颜色、样式属性(border-left-widthborder-top-colorborder-bottom-style)。因此, 即使computedStyle.border不会在所有浏览器中都返回值,但computedStyle.borderLeftWidth会返回值。

所有计算的样式都是只读的,计算后的样式也包含属于浏览器内部样式表的样式信息,因此任何具有默认值的CSS属性都会表现在计算后的样式中。

操作样式表

CSSStyleSheet类型表示的是样式表,使用document.styleSheets可以拿到所有包括通过<link>元素包含的样式表和在<style>元素中定义的样式表。

CSSStyleSheet继承自StyleSheet,从StyleSheet接口继承而来的属性如下。

  • disabled:表示样式表是否被禁用的布尔值。将这个值设置为true可以禁用样式表。
  • href:如果样式表是通过<link>包含的,则是样式表的URL;否则,是null<style>元素包含的样式表没有href属性
  • media:当前样式表支持的所有媒体类型的集合。也有一个length 属性和一个item()方法。也可以使用方括号语法取得集合中特定的项。如果集合是空列表,表示样式表适用于所有媒体。在IE 中,media是一个反映<link><style>元素media特性值的字符串。
  • ownerNode:指向拥有当前样式表的节点的指针,样式表可能是在HTML中通过<link><style/>引入的(在XML中可能是通过处理指令引入的)。如果当前样式表是其他样式表通过@import导入的,则这个属性值为nullIE 不支持这个属性。
  • parentStyleSheet:在当前样式表是通过@import导入的情况下,这个属性是一个指向导入它的样式表的指针。
  • titleownerNodetitle属性的值。
  • type:表示样式表类型的字符串。对CSS样式表而言,这个字符串是"type/css"

除了disabled属性之外,其他属性都是只读的

CSSStyleSheet类型自身的属性和方法如下,多与Rule有关

  • cssRules:样式表中包含的样式规则的集合。IE不支持这个属性,但有一个类似的rules属性。
  • ownerRule:如果样式表是通过@import导入的,这个属性就是一个指针,指向表示导入的规则;否则,值为nullIE不支持这个属性。
  • deleteRule(index):删除cssRules集合中指定位置的规则。IE不支持这个方法,但支持一个类似的removeRule()方法。
  • insertRule(rule,index):向cssRules集合中指定的位置插入rule字符串。IE不支持这个方法,但支持一个类似的addRule()方法。

不同浏览器的document.styleSheets返回的样式表也不同。所有浏览器都会包含<style>元素和rel特性被设置为"stylesheet"<link>元素引入的样式表。IEOpera也包含rel特性被设置为"alternate stylesheet"<link>元素引入的样式表。

也可以直接通过<link><style>元素取得CSSStyleSheet对象。DOM 规定了<link><style>元素包含了一个CSSStyleSheet对象的属性,名叫sheet;除了IE,其他浏览器都支持这个属性。IE 支持的是styleSheet 属性。

function getStyleSheet(element){
  return element.sheet || element.styleSheet;
}
//取得第一个<link/>元素引入的样式表
var link = document.getElementsByTagName("link")[0];
var sheet = getStylesheet(link);
CSS规则

CSSRule对象表示样式表中的每一条规则,例如 body { background-color:"red"}CSSRule是一个供其他多种类型继承的基类型,其中最常见的就是CSSStyleRule类型,表示样式信息(其他规则还有@import@font-face@page@charset,但这些规则很少有必要通过脚本来访问)

CSSStyleRule对象包含下列属性。

  • cssText:返回整条规则对应的文本,各浏览器的返回也不尽相同,IE 不支持这个属性。
  • parentRule:如果当前规则是导入的规则,这个属性引用的就是导入规则;否则,这个值为nullIE 不支持这个属性。
  • parentStyleSheet:当前规则所属的样式表。IE 不支持这个属性。
  • selectorText:返回当前规则的选择符文本。例如"#myDiv","h1",".color",在Firefox、Safari、Chrome 和IE 中这个属性是只读的。Opera 允许修改selectorText
  • style:一个CSSStyleDeclaration对象,可以通过它设置和取得规则中特定的样式值。
  • type:表示规则类型的常量值。对于样式规则,这个值是1。IE 不支持这个属性。

假设这条规则位于页面中的第一个样式表中。

div.box {
  background-color: blue;
  width: 100px;
  height: 200px;
}
var sheet = document.styleSheets[0];     // 取得样式表中的第一个
var rules = sheet.cssRules || sheet.rules;   //取得规则列表
var rule = rules[0];               //取得第一条规则
alert(rule.selectorText); //  div.box
alert(rule.style.cssText); //完整的CSS 代码
alert(rule.style.backgroundColor); //'blue'
alert(rule.style.width); //'100px'
alert(rule.style.height); //'200px'
rule.style.backgroundColor = "red"   
// 也可以通过rule 的 style设置样式 但是会应用到所有带有.box类的div上
创建规则
  • insertRule()和IE的addRule()

insertRule()接受两个参数:规则文本和表示在哪里插入规则的索引。
addRule()也接收两必选参数:选择符文本和CSS样式信息;一个可选参数:插入规则的位置。

var sheet = document.styleSheets[0];     // 取得样式表中的第一个
sheet.insertRule("body { background-color: silver }", 0); //DOM 方法
sheet.addRule("body", "background-color: silver", 0); //仅对IE 有效
删除规则
  • deleteRule()和IE的removeRule()

这两个方法接受一个参数:要删除的规则的位置。例如,要删除样式表中的第一条规则

sheet.deleteRule(0); //DOM 方法
sheet.removeRule(0); //仅对IE 有效

元素大小

1、偏移量

  • offsetHeight:元素在垂直方向上占用的空间大小,包括元素的高度、水平滚动条的高度、边框高度。
  • offsetWidth:元素在水平方向上占用的空间大小,包括元素的宽度、垂直滚动条的宽度、边框宽度。
  • offsetLeft:元素的左外边框至包含元素的左内边框之间的像素距离。
  • offsetTop:元素的上外边框至包含元素的上内边框之间的像素距离。

offset

要想知道某个元素在页面上的偏移量,可以通过循环至根元素累加它们的offsetLeft / offsetTop

function getElementLeft(element){
  var actualLeft = element.offsetLeft;
  var current = element.offsetParent;
  while (current !== null){
    actualLeft += current.offsetLeft;
    current = current.offsetParent;
  }
  return actualLeft;
}

所有这些偏移量属性都是只读的,而且每次访问它们都需要重新计算。因此,应该尽量避免重复访问这些属性;如果需要重复使用其中某些属性的值,可以将它们保存在局部变量中,以提高性能。

2、客户区大小

元素的客户区大小,指的是元素内容及其内边距所占据的空间大小。

  • clientWidth是元素内容区宽度加上左右内边距宽度;
  • clientHeight属性是元素内容区高度加上上下内边距高度。

client

// 判读是否是混杂模式来兼容IE,也是只读的,也是每次访问都要重新计算的。
function getViewport(){
  if (document.compatMode == "BackCompat"){  
    return {
      width: document.body.clientWidth,
      height: document.body.clientHeight
    };
  } else {
    return {
      width: document.documentElement.clientWidth,
      height: document.documentElement.clientHeight
    };
  }
}

3、滚动大小

滚动大小指的是包含滚动内容的元素的大小。有些元素(例如<html>元素),即使没有执行任何代码也能自动地添加滚动条;但另外一些元素,则需要通过CSSoverflow属性进行设置才能滚动。

  • scrollHeight:在没有滚动条的情况下,元素内容的总高度。
  • scrollWidth:在没有滚动条的情况下,元素内容的总宽度。
  • scrollLeft:被隐藏在内容区域左侧的像素数。通过设置这个属性可以改变元素的滚动位置。
  • scrollTop:被隐藏在内容区域上方的像素数。通过设置这个属性可以改变元素的滚动位置。

scrollWidthscrollHeight主要用于确定元素内容的实际大小。

scroll

4、确定元素大小

每个元素都提供了一个getBoundingClientRect()方法。这个方法返回会一个矩形对象,包含4 个属性:lefttoprightbottom。这些属性给出了元素在页面中相对于视口的坐标位置。(IE8及更早的版本将左上角坐标认定为(2,2),其他则认定为(0,0))

对于不支持getBoundingClientRect()的浏览器,可以通过其他手段取得相同的信息。一般来说,坐标轴中rightleft 差值与offsetWidth的值相等,而坐标轴中bottomtop的差值与offsetHeight相等,lefttop可以根据offsetLeftoffsetTop拿到(注意使用循环父元素,因为拿到的是距离父元素的,而不是距离文档边界的),而rightbottom就可以使用left + offsetWidthtop + offsetHeight拿到了。

三、遍历

“DOM2 级遍历和范围”模块定义了两个用于辅助完成顺序遍历DOM结构的类型:NodeIteratorTreeWalker。这两个类型能够基于给定的起点对DOM结构执行深度优先(depth-first)的遍历操作,IE 不支持DOM 遍历。

NodeIterator

NodeIterator类型是两者中比较简单的一个,可以使用document.createNodeIterator()方法创建它的新实例。这个方法接受下列4 个参数。

  • root:想要作为搜索起点的树中的节点。
  • whatToShow:表示要访问哪些节点的数字代码。
  • filter:是一个NodeFilter对象,或者一个表示应该接受还是拒绝某种特定节点的函数。
  • entityReferenceExpansion:布尔值,表示是否要扩展实体引用。这个参数在HTML页面中没有用,因为其中的实体引用不能扩展。
关于whatToShow

whatToShow参数是一个位掩码,通过应用一或多个过滤器(filter)来确定要访问哪些节点。这个参数的值以常量形式在NodeFilter类型中定义

  • NodeFilter.SHOW_ALL:显示所有类型的节点。
  • NodeFilter.SHOW_ELEMENT:显示元素节点。
  • NodeFilter.SHOW_ATTRIBUTE:显示特性节点。由于DOM结构原因,实际上不能使用这个值。
  • NodeFilter.SHOW_TEXT:显示文本节点。
  • NodeFilter.SHOW_CDATA_SECTION:显示CDATA节点。对HTML页面没有用。
  • NodeFilter.SHOW_ENTITY_REFERENCE:显示实体引用节点。对HTML 页面没有用。
  • NodeFilter.SHOW_ENTITYE:显示实体节点。对HTML 页面没有用。
  • NodeFilter.SHOW_PROCESSING_INSTRUCTION:显示处理指令节点。对HTML 页面没有用。
  • NodeFilter.SHOW_COMMENT:显示注释节点。
  • NodeFilter.SHOW_DOCUMENT:显示文档节点。
  • NodeFilter.SHOW_DOCUMENT_TYPE:显示文档类型节点。
  • NodeFilter.SHOW_DOCUMENT_FRAGMENT:显示文档片段节点。对HTML 页面没有用。
  • NodeFilter.SHOW_NOTATION:显示符号节点对HTML 页面没有用。

可以使用按位或操作符来组合多个选项

var whatToShow = NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT;
关于filter

createNodeIterator()方法的filter参数来指定自定义的NodeFilter对象(不使用上面说到的常量形式的),每个NodeFilter对象只有一个方法,即acceptNode();如果应该访问给定的节点,该方法返回NodeFilter.FILTER_ACCEPT,如果不应该访问给定的节点,该方法返回NodeFilter.FILTER_SKIP

由于NodeFilter是一个抽象的类型,因此不能直接创建它的实例。在必要时,只要创建一个包含acceptNode()方法的对象,然后将这个对象传入createNodeIterator()中即可。

下列代码展示了如何创建一个只显示<p>元素的节点迭代器。

var filter = {
  acceptNode: function(node){
      return node.tagName.toLowerCase() == "p" ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
  }
};
var iterator = document.createNodeIterator(root, NodeFilter.SHOW_ELEMENT, filter, false);

上面的通过document.createNodeIterator()方法得到的iterator ,就是NodeIterator类型,这个类型拥有nextNode()previousNode()这两个方法来遍历DOM子树,nextNode()方法用于向前前进一步,而previousNode()用于向后后退一步。在刚刚创建的NodeIterator对象中,有一个内部指针指向根节点,因此第一次调用nextNode()会返回根节点。向下遍历nextNode()到最后一个节点时返回null,向上遍历previousNode()也是一样,遍历到第一个节点时会返回null

<div id="div1">
  <p><b>Hello</b> world!</p>
  <ul>
    <li>List item 1</li>
    <li>List item 2</li>
    <li>List item 3</li>
  </ul>
</div>
var div = document.getElementById("div1");
var iterator = document.createNodeIterator(div, NodeFilter.SHOW_ELEMENT, null, false);
var node = iterator.nextNode(); // 拿到根元素div
while (node !== null) {
  alert(node.tagName); //输出标签名
  node = iterator.nextNode(); 
}

TreeWalker

TreeWalkerNodeIterator的一个更高级的版本。除了包括nextNode()previousNode()在内的相同的功能之外,这个类型还提供了下列用于在不同方向上遍历DOM结构的方法。

  • parentNode():遍历到当前节点的父节点;
  • firstChild():遍历到当前节点的第一个子节点;
  • lastChild():遍历到当前节点的最后一个子节点;
  • nextSibling():遍历到当前节点的下一个同辈节点;
  • previousSibling():遍历到当前节点的上一个同辈节点。

创建TreeWalker对象要使用document.createTreeWalker()方法,这个方法接受的4 个参数与document.createNodeIterator()方法相同:作为遍历起点的根节点、要显示的节点类型、过滤器和一个表示是否扩展实体引用的布尔值。

在这里,filter可以返回的值有所不同。除了NodeFilter.FILTER_ACCEPTNodeFilter.FILTER_SKIP 之外,还可以使用NodeFilter.FILTER_REJECT。使用SKIP会跳过当前节点,而使用REJECT会跳过当前节点及该节点的子节点。如果使用REJECT情况下,使用自定义filter过滤的时候,不包含根节点的话,将不会访问任何一个节点,因为nextNode()第一个访问的就是根节点,如果照之前定义的filter过滤只要p元素的,三元表达式第一次就会拿到false,取得REJECT,从而跳过第一个元素(根元素DIV)及其子节点,所以将访问不到任何一个节点。

TreeWalker真正强大的地方在于能够在DOM结构中沿任何方向移动。使用TreeWalker遍历DOM树,即使不定义过滤器,也可以取得所有<li>元素。

var div = document.getElementById("div1");
var walker = document.createTreeWalker(div, NodeFilter.SHOW_ELEMENT, null, false);
walker.firstChild(); //转到<p>
walker.nextSibling(); //转到<ul>
var node = walker.firstChild(); //转到第一个<li>
while (node !== null) {
  alert(node.tagName);
  node = walker.nextSibling();
}

TreeWalker类型还有一个属性,名叫currentNode,表示任何遍历方法在上一次遍历中返回的节点。通过设置这个属性也可以修改遍历继续进行的起点,如下面的例子所示。

var node = walker.nextNode();
alert(node === walker.currentNode); //true
walker.currentNode = document.body; //修改起点

四、范围

DOM2 级遍历和范围 模块定义了“范围”接口。通过范围可以选择文档中的一个区域,而不必考虑节点的界限(选择在后台完成,对用户是不可见的)。在常规的DOM 操作不能更有效地修改文档时,使用范围往往可以达到目的。

DOM2级在Document类型中定义了createRange()方法。在兼容DOM 的浏览器中,这个方法属于document对象。直接检测该方法可以确定浏览器是否支持范围。

var alsoSupportsRange = (typeof document.createRange == "function");

使用createRange()来创建DOM范围。

var range = document.createRange();

每个范围由一个Range类型的实例表示,这个实例拥有很多属性和方法:

  • startContainer:包含范围起点的节点(即选区中第一个节点的父节点)。
  • endContainer:包含范围终点的节点(即选区中最后一个节点的父节点)。
  • commonAncestorContainerstartContainerendContainer共同的祖先节点在文档树中位置最深的那个。
  • startOffset:范围在startContainer中起点的偏移量。如果startContainer是文本节点、注释节点或CDATA节点,那么startOffset就是范围起点之前跳过的字符数量。否则,startOffset就是范围中第一个子节点的索引。
  • endOffset:范围在endContainer中终点的偏移量(与startOffset遵循相同的取值规则)。

选择范围

  • selectNode()selectNodeContents()

这两个方法都接受一个参数,即一个DOM 节点,然后使用该节点中的信息来填充范围,selectNode()方法选择整个节点,包括其子节点;而selectNodeContents()方法则只选择节点的子节点。

range

<body>
  <p id="p1">
    <b>Hello</b> world!
  </p>
</body>

<script>
  var range1 = document.createRange();
  var range2 = document.createRange();
  var p1 = document.getElementById("p1");
  range1.selectNode(p1);
  console.log(range1.startContainer); // body 传入节点的父节点
  console.log(range1.endContainer);   // body
  console.log(range1.commonAncestorContainer); // body

  // 打印出1 --> 给定节点在其父节点的childNodes中的索引(body与p之间的空格算作文本节点)
  console.log(range1.startOffset); 

  // 打印出2  --> 等于startOffset + 范围中选择中的节点个数(只选择了一个p元素)
  console.log(range1.endOffset);  

  range2.selectNodeContents(p1);
  console.log(range2.startContainer); // p 等于传入的节点即p元素
  console.log(range2.endContainer);   // p
  console.log(range2.commonAncestorContainer); // p
  console.log(range2.startOffset); // 0 始终为0,因为范围是从给定节点的第一个子节点开始的
  console.log(range2.endOffset);  // 2  等于给点节点的子节点数量 node.children.length
</script>
  • setStartBefore(refNode)

将范围的起点设置在refNode之前,因此refNode也就是范围选区中的第一个子节点。同时会将startContainer属性设置为refNode.parentNode,将startOffset属性设置为refNode在其父节点的childNodes集合中的索引。

  • setStartAfter(refNode)

将范围的起点设置在refNode之后,因此refNode也就不在范围之内了,其下一个同辈节点才是范围选区中的第一个子节点。同时会将startContainer属性设置为refNode.parentNode,将startOffset属性设置为refNode在其父节点的childNodes集合中的索引加1。

  • setEndBefore(refNode)

将范围的终点设置在refNode之前,因此refNode也就不在范围之内了,其上一个同辈节点才是范围选区中的最后一个子节点。同时会将endContainer属性设置为refNode.parentNode,将endOffset属性设置为refNode在其父节点的childNodes集合中的索引。(即最后一个子节点索引加1)

  • setEndAfter(refNode)

将范围的终点设置在refNode之后,因此refNode也就是范围选区中的最后一个子节点。同时会将endContainer属性设置为refNode.parentNode,将endOffset属性设置为refNode在其父节点的childNodes集合中的索引加1。

复杂选择

  • setStart()setEnd()方法

这两个方法都接受两个参数:一个参照节点和一个偏移量值,对setStart()来说,参照节点会变成startContainer,而偏移量值会变成startOffset。对于setEnd()来说,参照节点会变成endContainer,而偏移量值会变成endOffset

假设你只想选择前面HTML示例代码中从"Hello""llo""world!""o"llo</b> wo

<p id="p1"><b>Hello</b> world!</p>

<script>
  var p1 = document.getElementById("p1");
  var helloNode = p1.firstChild.firstChild; // p第一个子元素b的第一个子节点(即文本节点hello)
  var worldNode = p1.lastChild; // p的第二个子节点(即文本节点 world!)

  var range = document.createRange();
  range.setStart(helloNode, 2);
  range.setEnd(worldNode, 3);
</script>

range.2

即选择了llo</b> wo,但是,范围知道自身缺少哪些开标签和闭标签,它能够重新构建有效的DOM结构以便我们对其进行操作。DOM就会变成 <p><b>He</b><b>llo</b> world!</p>,在此例中,会为范围内的llo</b>添加开标签,也会为范围外的<b>He添加闭合标签。

操作DOM 范围

  • deleteContents()

这个方法能够从文档中删除范围所包含的内容,上面的例子中如果在最后调用range.deleteContents(); ,DOM就会变成

<p><b>He</b>rld!</p>

由于范围选区在修改底层DOM结构时能够保证格式良好,因此即使内容被删除了,最终的DOM结构依旧是格式良好的。在此例中就是会为范围外的<b>He添加闭合标签。

  • extractContents()

这个方法也会从文档中移除范围选区。但区别在于extractContents()会返回范围的文档片段。

var fragment = range.extractContents();
p1.parentNode.appendChild(fragment);

最终DOM就会变成

<p><b>He</b>rld!</p>
<b>llo</b> wo
  • cloneContents()

创建范围对象的一个副本,然后在文档的其他地方插入该副本。不会移除范围选区

var fragment = range.cloneContents();
p1.parentNode.appendChild(fragment);

最终DOM就会变成

<p><b>Hello</b> world!</p>
<b>llo</b> wo
// 跟使用删除的方法不同, p元素的选中范围是没有被移除的
  • insertNode()

可以向范围选区的开始处插入一个节点。

var span = document.createElement("span");
span.style.color = "red";
span.appendChild(document.createTextNode("Inserted text"));
range.insertNode(span);

最终DOM会变成

<p id="p1"><b>He<span style="color: red">Inserted text</span>llo</b> world</p>
  • surroundContents()

这个方法接受一个参数,即环绕范围内容的节点。在环绕范围插入内容时,后台会执行下列步骤。

  1. 提取出范围中的内容(类似执行extractContent());
  2. 给定节点插入到文档中原来范围所在的位置上
  3. 文档片段的内容添加到给定节点中。

可以使用这种技术来突出显示网页中的某些词句

<p id="p1"><b>Hello</b> world!</p>

<script>
  var p1 = document.getElementById("p1");
  var helloNode = p1.firstChild.firstChild;
  range = document.createRange();

  range.selectNode(helloNode);
  var span = document.createElement("span");
  span.style.backgroundColor = "yellow";
  range.surroundContents(span)
</script>

最终DOM会变成,"Hello"这个单词会变成黄色背景,被标签span包裹。

<p><b><span style="background-color:yellow">Hello</span></b> world!</p>
<!-- 为了插入<span>,范围必须包含整个DOM 选区(不能仅仅包含选中的DOM 节点)。 -->
  • collapse()折叠DOM

使用collapse()方法来折叠范围(可以理解为清除范围,即范围起点终点重合,范围中什么都没有),这个方法接受一个参数,一个布尔值,表示要折叠到范围的哪一端。参数true表示折叠到范围的起点,参数false表示折叠到范围的终点。要确定范围已经折叠完毕,可以检查collapsed属性

range.collapse(true); //折叠到起点
alert(range.collapsed); //输出true

检测某个范围是否处于折叠状态,可以帮我们确定范围中的两个节点是否紧密相邻。即这个范围是否被清除,是否什么都没有

<p id="p1">Paragraph 1</p><p id="p2">Paragraph 2</p>

<script>
var p1 = document.getElementById("p1");
var p2 = document.getElementById("p2");
var range = document.createRange();
range.setStartAfter(p1); // 将起点设置在p1后面
range.setStartBefore(p2); // 将终点设置在p2前面
alert(range.collapsed); //输出true --> p1的后面和p2的前面什么也没有。
</script>
  • compareBoundaryPoints() 比较范围

这个方法接受两个参数:表示比较方式的常量值和要比较的范围。比较方式的常量值如下,

例:A.compareBoundaryPoints(Range.xxxx,B)

  • Range.START_TO_START:比较A和B的起点;(A头 与 B头 )
  • Range.START_TO_END:比较A的起点和B的终点;(A头 与 B尾)
  • Range.END_TO_END:比较A和B的终点;(A尾 与 B尾)
  • Range.END_TO_START:比较B的终点和A的起点。(A尾 与 B头)

compareBoundaryPoints()方法可能的返回值如下:

如果第一个范围中的点位于第二个范围中的点之前,返回-1;如果两个点相等,返回0;如果第一个范围中的点位于第二个范围中的点之后,返回1。

  • cloneRange()复制范围

这个方法会创建调用它的范围的一个副本。新创建的范围与原来的范围包含相同的属性,而修改它的端点不会影响原来的范围。

var newRange = range.cloneRange();
  • detach() 清理范围

在使用完范围之后,最好是调用detach()方法,以便从创建范围的文档中分离出该范围。调用detach()之后,就可以放心地解除对范围的引用,从而让垃圾回收机制回收其内存了

//  不再是清除范围,而是在文档中直接清理出去。
range.detach(); //从文档中分离 
range = null; //解除引用