本篇内容
  • DOM遍历
  • DOM范围

3 遍历

“DOM2级遍历和范围”模块定义了两个用于辅助完成顺序遍历DOM结构的类型:Nodeiterator和TreeWalker。这两个类型能够基于给定的起点对DOM结构执行深度优先(depth-first)的遍历操作。在与DOM兼容的浏览桥中(Firefox1及更高版本、Safari1.3及更高版本、Opera7.6及更高版本、Chrome0.2及更高版本),都可以访问到这些类型的对象。IE不支持DOM遍历。使用下列代码可以检测浏览器对DOM2级遍历能力的支持情况。

1
2
3
var supportsTraversals = document.implementation.hasFeature("Traversal", "2.0");
var supportsNodeiterator = (typeof document.createNodeiterator == "function");
var supportsTreeWalker = (typeof document.createTreeWalker == "function");

DOM遍历是深度优先的DOM结构遍历,也就是说,移动的方向至少有两个(取决于使用的遍历类型)。遍历以给定节点为跟,不可能向上超出DOM树的根节点。

3.1 NodeIterator

可以使用document.createNodeIterator()方法创建它的新实例。这个方法接受4个参数。

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

whatToShow参数是一个位掩码,通过应用一或多个过滤器(filter)来确定要访问哪些节点。这个参数的值以常量形式在NodeFilter类型中定义。
可以通过createNodeiterator()方法的filter参数来指定自定义的NodeFilter对象,或者指定一个功能类似节点过滤器(node filter)的函数。每个NodeFilter对象只有一个方法,即acceptNode();如果应该访问给定的节点,该方法返回NodeFilter.FILTER_ACCEPT,如果不应该访问给定的节点,该方法返回NodeFilter.FILTER_SKIP。由于NodeFilter是-个抽象的类型,因此不能直接创建它的实例。在必要时,只要创建一千包含acceptNode()方法的对象,然后将这个对象传入createNodeiterator()中即可。

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

1
2
3
4
5
6
7
8
9
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);

第三个参数也可以是一个与acceptNode()方法类似的函数,如

1
2
3
4
5
6
7
var filter = function (node) {
return node.tagName.toLowerCase() === 'p' ?
NodeFilter.FILTER_ACCEPT :
NodeFilter.FILTER_SKIP;
};

var iterator = document.createNodeIterator(document, NodeFilter.SHOW_ELEMENT, filter, false);

如果不指定过滤器,那么应在第三个参数的位置上传入null。

Nodeiterator类型的两个主要方法是 nextNode()和previousNode()。
在深度优先的DOM子树遍历中,nextNode()方法用于向前前进一步,而previousNode()用于向后后退一步。在刚刚创建的Nodeiterator对象中,有一个内部指针指向根节点,因此第-次调用nextNode()会返回根节点。当遍历到DOM子树的最后一个节点时,nextNode()返回null。previousNode()方法的工作机制类似。当遍历到DOM子树的最后一个节点,且previousNode()返回根节点之后,再次调用它就会返回null。

以下面的HTML片段为例

1
2
3
4
5
6
7
8
<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>

如果要遍历<div>中的所有元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var div = document.getElementById('div1');
var iterator = document.createNodeIterator(div, NodeFilter.SHOW_ELEMENT, null, false);
var node = iterator.nextNode();
while (node !== null) {
alert(node.tagName);
node = iterator.nextNode();
}

/* result
DIV
P
B
UL
LI
LI
LI
*/

如果只想返回遇到的<li>元素,则

1
2
3
4
5
6
7
8
9
10
11
12
var div = document.getElementById('div1');
var filter = function (node) {
return node.tagName.toLowerCase() === 'li' ?
NodeFilter.FILTER_ACCEPT :
NodeFilter.FILTER_SKIP;
};
var iterator = document.createNodeIterator(div, NodeFilter.SHOW_ELEMENT, filter, false);
var node = iterator.nextNode();
while (node !== null) {
alert(node.tagName);
node = iterator.nextNode();
}

由于nextNode()和previousNode()方法都基于Nodeiterator在DOM结构中的内部指针工作,所以DOM结构的变化会反映在遍历的结果中。

3.2 TreeWalker

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

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

创建TreeWalker对象要使用document.createTreeWalker()方法,这个方法接受的4个参数与document.createNodeiterator()方法相同:作为遍历起点的根节点、要显示的节点类型、过滤器和一个表示是否扩展实体引用的布尔值。由于这两个创建方法很相似,所以很容易用TreeWalker来代替Nodeiterator, 如下面的例子所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
var div = document.getElementById('div1');
var filter = function (node) {
return node.tagName.toLowerCase() === 'li' ?
NodeFilter.FILTER_ACCEPT :
NodeFilter.FILTER_SKIP;
};

var walker = document.createTreeWalker(div, NodeFilter, SHOW_ELEMENT, filter, false);
var node = iterator.nextNode();
while (node !== null) {
alert(node.tagName);
node = iterator.nextNode();
}

在这里,filter可以返回的值有所不同。除了NodeFilter.FILTER_ACCEPT和NodeFilter.FILTER_SKIP之外,还可以使用NodeFilter.FILTER_REJECT。在使用Nodeiterator对象时,NodeFilter.FILTER_SKIP与NodeFilter.FILTER_REJECT的作用相同:跳过指定的节点。但在使用treeWalker对象时,NodeFilter.FILTER_SKIP会跳过相应节点继续前进到子树中的下一个节点,而NodeFilter.FILTER_REJECT则会跳过相应节点及该节点的整个子树。
当然,TreeWalker真正强大的地方在于能够在DOM结构中沿任何方向移动。使用TreeWalker遍历DOM树,即使不定义过滤器,也可以取得所有<li>元素

1
2
3
4
5
6
7
8
9
10
11
var div = document.getElementById('div1');
var walker = document.createTreeWalker(div, NodeFilter.SHOW_ELEMENT, null, false);

walker.firstChild(); // 转到<p>
walker.nextChild(); // 转到<ul>

var node = walker.firstChild(); // 转到第一个<li>
while (node !== null) {
alert(node.tagName);
node = iterator.nextNode();
}

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

1
2
3
var node = walker.nextNode();
console.log(node === walker.currentNode); // true
walker.currentNode = document.body; // 修改起点

与Nodeiterator相比,TreeWalker类型在遍历DOM时拥有更大的灵活性。由于IE中没有对应的类型和方法,所以使用遍历的跨浏览器解决方案非常少见。

4 范围

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

4.1 DOM中的范围

DOM2级在Document类型中定义了createRange()方法。在兼容DOM的浏览器中,这个方法属于document对象。检测该方法

1
2
var supportsRange = document.implementation.hasFeature('Range', '2.0'); 
var alsoSupportsRange = (typeof document.createRange == 'function');

创建DOM范围

1
var range = document.createRange();

与节点类似,新创建的范围也直接与创建它的文挡关联在起,不能用于其他文挡。创建了范围之后.接下来就可以使用它在后台选择文档中的特定部分。而创建范围并设置了其位置之后,还可以针对泡围的内容执行很多种操作,从而实现对底层DOM树的更精细的控制。

每个范围由一个Range类型的实例表示,这个实例拥有很多属性和方法。下列属性提供了当前范围在文档中的位置信息。

  • startContainer:包含范围起点的节点(即选区中第一个节点的父节点)。
  • startOffset: 范围在startContainer中起点的偏移量。
  • endContainer:包含范围终点的节点(即选区中最后一个节点的父节点)。
  • endOffset:范围在endContainer中终点的偏移量(与startOffset遵循相同的取值规则)。
  • commonAncestorContainer: startContainer和endContainer共同的祖先节点在文档树中位置最深的那个。

4.1.1 用DOM范围实现简单选择

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

为了更精细地控制将哪些节点包含在范围中,还可以使用下列方法。

  • setStartBefore(refNode):将范围的起点设置在refNode之前,因此refNode也就是范围选区中的第一个子节点。同时会将startContainer属性设置为refNode.parentNode,将startOffset属性设置为refNode在其父节点的childNodes集合中的索引。
  • setStartAfter(refNode):将范围的起点设置在refNode之后,因此refNode也就不在范围之内了,其下一个同辈节点才是范围选区中的第一个子节点。同时会将startContainer属性设置为 refNode.parentNode 将startOffset 属性设置为 refNode 在其父节点的childNodes集合中的索引加l。
  • setEndBefore(refNode):将范围的终点设置在refNode之前,因此refNode也就不在起围之内了,其上一个同辈节点才是范围选区中的最后一个子节点。同时会将endContainer属性设置为refNode.parentNode,将endOffse仨属性设置为refNode在其父节点的childNodes集合中的索引。
  • setEndAfter (refNode):将范围的终点设置在refNode之后,因此refNode也就是范围选区中的最后一个子节点。同时会将 endContainer属性设置为refNode.parentNode,将endOffset属性设置为refNode在其父节,点的childNodes集合中的索引加1。

4.1.2 用DOM范围实现复杂选择

要创建复杂的范围就得使用setStart()和setEnd()方法。这两个方法都接受两个参数:一个照节点和一个偏移量值。对setStart()来说,参照节点会变成startContainer,而偏移量值会变成startOffset。对于setEnd()来说,参照节点会变成endContainer,而偏移最值会变成endOffset。可以使用这两个方法来模仿selectNode()和selectNodeContents()。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var range1 = document.createRange();
var range2 = document.createRange();
var p1 = document.getElementById('p1');
var p1Index = -1;
var i,
len;

for (i = 0, len = p1.parentNode.childNodes.length; i < len; i++) {
if (p1.parentNode.childNodes[i] == p1) {
p1Index = i;
break;
}
}

range1.setStart(p1.parentNode, p1Index);
range1.setEnd(p1.parentNode, p1Index + 1);
range2.setStart(p1, 0);
range2.setEnd(p1, p1.childNodes.length);

4.1.3 操作DOM范围中的内容

在创建范围时,内部会为这个范围创建一个文档片段,范围所属的全部节点都被添加到了这个文档片段中。为了创建这个文档片段,范围内容的格式必须正确有效。在前面的例子中,我们创建的选区分别开始和结束于两个文本节点的内部,因此不能算是格式良好的DOM结构,也就无法通过DOM来表示。但是,范围知道自身缺少哪些开标签和闭标签,它能够重新构建有效的DOM结构以便我们对其进行操作。

deleteContents()方法能够从文档中删除范围所包含的内容。
如:

1
2
3
4
5
6
7
8
var p1 = document.getElementById('p1');
var helloNode = p1.firstChild.firstChild;
var worldNode = p1.lastChild;
var range = document.createRange();
range.setStart(helloNode, 2);
range.setEnd(worldNode, 3);

range.deleteContents();

与deleteContents()方法相似,extractContents()也会从文梢中移除范围选区。但这两个方法的区别在于,extractContents()会返回范围的文档片段(返回的文档片段包含的是范围中节点的副本,而不是实际的节点)。利用这个返回的值,可以将范围的内容插入到文档中的其他地方。

1
2
3
4
5
6
7
8
9
10
var p1 = document.getElementById('p1');
var helloNode = p1.firstChild.firstChild;
var worldNode = p1.lastChild;
var range = document.createRange();

range.setStart(helloNode, 2);
range.setEnd(worldNode, 3);

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

4.1.4 插入DOM范围中的内容

利用范围可以删除或复制内容,还可以像前面介绍的那样操作范围中的内容。使用insertNode()方法可以向范围选区的开始处插入一个节点。

1
2
3
4
var span = document.createElement('span'); 
span.style.color = 'red';
span.appendChild(document.createTextNode('Inserted text'));
range.insertNode(span);

除了向范围内部插入内容之外,还可以环绕范围插入内容,此时就要使用surroundContents()方法。 这个方法接受一个参数,即环绕范围内容的节点。在环绕范围插入内容时,后台会执行下列步骤。

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

4.1.5 折叠DOM范围

所谓折叠就是指范围中未选择文挡的任何部分。可以用文本框来描述折叠范围的过程。
collapse()方法来折叠范围,这个方法接受一个参数,一个布尔值,表示要折叠到范围的哪一端。参数true表示折叠到范围的起点。参数false表示折叠到范围的终点。要确定范围已经折叠完毕.可以检查collapsed属性,如下

1
2
range.collapse(true);   // 折叠到起点
console.log(range.collapsed); // true

4.1.6 比较DOM范围

在有多个范围的情况下,可以使用compareBoundaryPoints()方法来确定这些范围是否有公共的边界(起点或终点)。这个方法接受两个参数:表示比较方式的常量值和要比较的范围。表示比较方式的常量值如下所示。

  • Range.START_TO_START(O):比较第一个范围和第二个范围的起点;
  • Range.START_TO_END(1):比较第一个范围的起点和第二个范围的终点;
  • Range.END_TO_END(2):比较第一个范围和第二个范围的终点;
  • Range.END_TO_START(3):比较第一个范围的终点和第一个范围的起点。

compareBoundaryPoints()方法可能的返回值如下:如果第一个范围中的点位于第二个范围中的点之前,返回-1;如果两个点相等,返回0;如果第一个范围中的点位于第二个范围中的点之后,返回1。

4.1.7 复制DOM范围

可以使用cloneRange()方法复制范围。这个方法会创建调用它的范围的一个副本。
新创建的范围与原来的范倒包含糊同的属性,而修改它的端点不会影响原来的范围。

4.1.8 清理DOM范围

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

1
2
range.detach(); // 从文档中分离
range = null;

4.2 IE8及更早版本中的范围

IE8及早期版本支持一种类似的概念,即文本范围(text range)。文本范围是IE专有的特性,其他浏览器都不支排。文本范围处理的主要是文本(不一定是DOM节点)。

4.2.1 用IE范围实现简单的选捧

选择页面中某一区域的最简单方式,就是使用范围的findText()方法。这个方法会找到第一次出现的给定文本,并将范围移过来以环绕该文本。如果没有找到文本,这个方法返回false;否则返回true。

还可以为findText()传入另一个参数,即一个表示向哪个方向继续搜索的数值。负值表示应该从当前位置向后搜索,而正值表示应该从当前位置向前搜索。
IE中与DOM中的selectNode()方法最接近的方法是moveToElernentText(),这个方法接受一个DOM元素,并选择该元素的所有文本,包括HTML标签。在文本范罔中包含HTh伍的情况下,可以使用htmlText属性取得范围的全部内容,包括HTML 和文本IE的范围没有任何属性可以随着范围选区的变化而动态更新。不过,其parentElement()方法倒是与DOM的comrnonAncestorCon巳ainer属性类似。

4.2.2 使用IE范围实现复杂的选择

在归中创建复杂范围的方法,就是以特定的增量向四周移动范围:move()、moveStart()、moveEnd()和expand()。这些方法都接受两个参数:移动单位和移动单位的数量。其中,移动单位是下列一种字符串值。

  • character: 逐个字符地移动。
  • word: 逐个单词(一系列非空格字符)地移动。
  • sentence: 逐个句子(一系列以句号、问号或叹号结尾的字符)地移动。
  • textedit: 移动到当前范围选区的开始或结束位置。
    通过moveStart()方法可以移动范罔的起点,通过moveEnd()方法可以移动范围的终点,移动幅度由单位数量指定使用expand()方法可以将范围规范化。换句话说,expand()方法的作用是将任何部分选择的文本全部选中。例如,当前选择的是一个单词中间的两个字符,调用expand(‘word’)可以将整个单词都包含在范围之内。

而move()方法则首先会折叠当前范围(让起点和终点相等),然后再将范围移动指定的单位数量。
调用move()之后,范围的起点和终点相间,因此必须再使用moveStart()或moveEnd()创建新的选区。

4.2.3 操作IE范围中的内容

在IE中操作范围巾的内容可以使用text属性或pasteHTML()方法。

4.2.4 折叠IE范围

皿为范围提供的collapse()方法与相应的DOM方法用法一样:传入true把范围折叠到起点,传人false把范围折叠到终点。
可惜的是,没有对应的collapsed属性让我们知道范围是否已经折叠完毕。为此,必须使用boundingWidth属性,该属性返回范围的宽度(以像素为单位λ如果boundingWidth属性等于0,就说明范围已经忻叠了)

4.2.5 比较IE范围

IE中的compareEndPoints()方法与DOM范围的compareBoundaryPoints()方法类似。这个方法接受两个参数:比较的类型和要比较的范围。比较类型的取值范围是下列几个字符串值:”StartToStart” 、”StartToEnd”、”EndToEnd” 和”EndToStart”。这几种比较类型与比较DOM范围时使用的几个值是相同的。

同样与DOM类似的是,compareEndPoints()方法也会接照相同的规则返回值.即如果第一个范围的边界位于第二个范围的边界前面,返回-1;如果二者边界相同, 返回0;如果第一个范围的边界位 于第二个范围的边界后面,返回1。

IE中还有两个方法,也是用于比较范围的:isEqual()用于确定两个范围是否相等,inRange()用于确定一个范围是否包含另一个范围。

4.2.6 复制IE范围

在IE中使用duplicate()方法可以复制文本范围,结果会创建原范围的一个副本

温习:

  • DOM遍历:NodeIterator、TreeWalker
  • DOM范围:创建、选择、操作
  • 低版本IE的DOM范围

(完)