一、遍历
“DOM2 级遍历和范围”模块定义了两个用于辅助完成顺序遍历DOM
结构的类型:NodeIterator
和TreeWalker
。这两个类型能够基于给定的起点对DOM
结构执行深度优先(depth-first
)的遍历操作。IE 不支持DOM 遍历。使用下列代码可以检测浏览器对DOM2
级遍历能力的支持情况。
var supportsNodeIterator = (typeof document.createNodeIterator == "function");
var supportsTreeWalker = (typeof document.createTreeWalker == "function");
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()
中即可。
下列代码展示了如何创建一个只显示元素的节点迭代器。
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()
方法类似的函数
var filter = function(node){
return node.tagName.toLowerCase() == "p" ?
NodeFilter.FILTER_ACCEPT :
NodeFilter.FILTER_SKIP;
};
如果不指定过滤器,那么应该在第三个参数的位置上传入null。
var iterator = document.createNodeIterator(document, NodeFilter.SHOW_ALL,null, false);
上面的通过document.createNodeIterator()
方法得到的iterator
,就是NodeIterator
类型,这个类型拥有nextNode()
和previousNode()
这两个方法来遍历DOM子树,nextNode()
方法用于向前前进一步,而previousNode()
用于向后后退一步。在刚刚创建的NodeIterator
对象中,有一个内部指针指向根节点,因此第一次调用nextNode()
会返回根节点。向下遍历nextNode()
到最后一个节点时返回null
,向上遍历previousNode()
也是一样,遍历到第一个节点时会返回null
HTML
Hello world!
- List item 1
- List item 2
- List item 3
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();
// 这里再次使用nextNode拿到p元素 依次向下遍历
}
如果只想遍历其中的p元素,就可以使用第三个参数filter
,不再传值null
,而是传一个与acceptNode()
方法类似的函数
TreeWalker
TreeWalker
是NodeIterator
的一个更高级的版本。除了包括nextNode()
和previousNode()
在内的相同的功能之外,这个类型还提供了下列用于在不同方向上遍历DOM
结构的方法。
-
parentNode()
:遍历到当前节点的父节点; -
firstChild()
:遍历到当前节点的第一个子节点; -
lastChild()
:遍历到当前节点的最后一个子节点; -
nextSibling()
:遍历到当前节点的下一个同辈节点; -
previousSibling()
:遍历到当前节点的上一个同辈节点。
创建TreeWalker
对象要使用document.createTreeWalker()
方法,这个方法接受的4 个参数与document.createNodeIterator()
方法相同:作为遍历起点的根节点、要显示的节点类型、过滤器和一个表示是否扩展实体引用的布尔值。
在这里,filter
可以返回的值有所不同。除了NodeFilter.FILTER_ACCEPT
和NodeFilter.FILTER_SKIP
之外,还可以使用NodeFilter.FILTER_REJECT
。使用SKIP
会跳过当前节点,而使用REJECT
会跳过当前节点及该节点的子节点。如果使用REJECT
情况下,使用自定义filter
过滤的时候,不包含根节点的话,将不会访问任何一个节点,因为nextNode()
第一个访问的就是根节点,如果照之前定义的filter
过滤只要p元素的,三元表达式第一次就会拿到false
,取得REJECT
,从而跳过第一个元素(根元素DIV)及其子节点,所以将访问不到任何一个节点。
TreeWalker
真正强大的地方在于能够在DOM
结构中沿任何方向移动。使用TreeWalker
遍历DOM
树,即使不定义过滤器,也可以取得所有元素。
var div = document.getElementById("div1");
var walker = document.createTreeWalker(div, NodeFilter.SHOW_ELEMENT, null, false);
walker.firstChild(); //转到
walker.nextSibling(); //转到
var node = walker.firstChild(); //转到第一个-
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 级遍历和范围”
模块定义了“范围”(range
)接口。通过范围可以选择文档中的一个区域,而不必考虑节点的界限(选择在后台完成,对用户是不可见的)。在常规的DOM 操作不能更有效地修改文档时,使用范围往往可以达到目的。Firefox、Opera、Safari 和Chrome 都支持DOM
范围。IE 以专有方式实现了自己的范围特性。
DOM2
级在Document
类型中定义了createRange()
方法。在兼容DOM 的浏览器中,这个方法属于document
对象。直接检测该方法可以确定浏览器是否支持范围。
var alsoSupportsRange = (typeof document.createRange == "function");
使用createRange()
来创建DOM范围
var range = document.createRange();
每个范围由一个Range
类型的实例表示,这个实例拥有很多属性和方法。下列属性提供了当前范围在文档中的位置信息。
startContainer
:包含范围起点的节点(即选区中第一个节点的父节点)。endContainer
:包含范围终点的节点(即选区中最后一个节点的父节点)。commonAncestorContainer
:startContainer
和endContainer
共同的祖先节点在文档树中位置最深的那个。startOffset
:范围在startContainer
中起点的偏移量。如果startContainer
是文本节点、注释节点或CDATA
节点,那么startOffset
就是范围起点之前跳过的字符数量。否则,startOffset
就是范围中第一个子节点的索引。endOffset
:范围在endContainer
中终点的偏移量(与startOffset
遵循相同的取值规则)。
选择范围
-
selectNode()
或selectNodeContents()
这两个方法都接受一个参数,即一个DOM 节点,然后使用该节点中的信息来填充范围,selectNode()
方法选择整个节点,包括其子节点;而selectNodeContents()
方法则只选择节点的子节点。
html:
Hello world!
js:
var range1 = document.createRange();
var range2 = document.createRange();
var p1 = document.getElementById("p1");
range1.selectNode(p1);
console.log(range1.startContainer); // 传入节点的父节点即document.body
console.log(range1.endContainer); // 传入节点的父节点即document.body
console.log(range1.commonAncestorContainer); // 传入节点的父节点即document.body
console.log(range1.startOffset); // 1 --> 给定节点p元素在其父节点的childNodes 集合中的索引(body->p之间的空格算作一个文本节点)
console.log(range1.endOffset); // 2 --> 等于startOffset + 范围中选择中的节点个数(只选择了一个p元素)
range2.selectNodeContents(p1);
console.log(range2.startContainer); // 等于传入的节点即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
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 wo
例1
html:
Hello world!
js:
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);
rang e.setEnd(worldNode, 3);
即选择了llo wo
,但是,范围知道自身缺少哪些开标签和闭标签,它能够重新构建有效的DOM
结构以便我们对其进行操作。DOM就会变成
Hello world!
在此例中,会为范围内的llo
添加开标签,也会为范围外的He
添加闭合标签。
操作DOM 范围
deleteContents()
这个方法能够从文档中删除范围所包含的内容,上面的例子中如果在最后调用range.deleteContents();
,DOM就会变成
Herld!
由于范围选区在修改底层DOM
结构时能够保证格式良好,因此即使内容被删除了,最终的DOM
结构依旧是格式良好的。在此例中就是会为范围外的He
添加闭合标签。
extractContents()
这个方法也会从文档中移除范围选区。但区别在于extractContents()
会返回范围的文档片段。
// 在例1的基础上添加如下代码
var fragment = range.extractContents();
p1.parentNode.appendChild(fragment);
最终DOM就会变成
Herld!
llo wo
cloneContents()
创建范围对象的一个副本,然后在文档的其他地方插入该副本。不会移除范围选取
// 在例1的基础上添加如下代码
var fragment = range.cloneContents();
p1.parentNode.appendChild(fragment);
最终DOM就会变成
Hello world!
llo wo
// 跟使用删除的方法不同, p元素的选中范围是没有被移除的
insertNode()
可以向范围选区的开始处插入一个节点。
// 在例1的基础上添加如下代码
var span = document.createElement("span");
span.style.color = "red";
span.appendChild(document.createTextNode("Inserted text"));
range.insertNode(span);
最终DOM会变成
HeInserted textllo world
surroundContents()
这个方法接受一个参数,即环绕范围内容的节点。在环绕范围插入内容时,后台会执行下列步骤。
- 提取出范围中的内容(类似执行extractContent());
- 将给定节点插入到文档中原来范围所在的位置上;
- 将文档片段的内容添加到给定节点中。
可以使用这种技术来突出显示网页中的某些词句
html:
Hello world!
js:
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)
最终DOM会变成,"Hello"
这个单词会变成黄色背景,被标签span
包裹。
Hello world!
// 为了插入,范围必须包含整个DOM 选区(不能仅仅包含选中的DOM 节点)。
-
collapse()
折叠DOM
使用collapse()
方法来折叠范围(可以理解为清除范围,即范围起点终点重合,范围中什么都没有),这个方法接受一个参数,一个布尔值,表示要折叠到范围的哪一端。参数true
表示折叠到范围的起点,参数false
表示折叠到范围的终点。要确定范围已经折叠完毕,可以检查collapsed
属性
range.collapse(true); //折叠到起点
alert(range.collapsed); //输出true
检测某个范围是否处于折叠状态,可以帮我们确定范围中的两个节点是否紧密相邻。即这个范围是否被清除,是否什么都没有
html:
Paragraph 1
Paragraph 2
js:
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 的前面什么也没有。
-
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; //解除引用
三、IE中的范围
IE8 及之前版本不支持DOM
范围。不过,IE8 及早期版本支持一种类似的概念,即文本范围(text range
),IE专有。
var range = document.body.createTextRange(); // 创建范围
选择范围
使用范围的findText()
方法。这个方法会找到第一次出现的给定文本,并将范围移过来以环绕该文本。如果没有找到文本,这个方法返回false
;否则返回true
。
html:
Hello world!
js:
var range = document.body.createTextRange();
var found = range.findText("Hello");
// 选中"Hello"
alert(found); //true
alert(range.text); //"Hello"
还可以为findText()
传入另一个参数,即一个表示向哪个方向继续搜索的数值。负值表示应该从当前位置向后搜索,而正值表示应该从当前位置向前搜索。
moveToElementText()
moveToElementText()
,这个方法接受一个DOM
元素,并选择该元素的所有文本,包括HTML
标签。类似于selectNode()
(selectNode()
方法选择整个节点,包括其子节点)
var range = document.body.createTextRange();
var p1 = document.getElementById("p1");
range.moveToElementText(p1);
在文本范围中包含HTML
的情况下,可以使用htmlText
属性取得范围的全部内容,alert(range.htmlText);
复杂选择
IE 提供了4 个方法以特定的增量向四周移动范围。这些方法都接受两个参数:移动单位和移动单位的数量,移动单位是下列一种字符串值。
-
"character"
:逐个字符地移动。 -
"word"
:逐个单词(一系列非空格字符)地移动。 -
"sentence"
:逐个句子(一系列以句号、问号或叹号结尾的字符)地移动。 -
"textedit"
:移动到当前范围选区的开始或结束位置。
moveStart()
:移动范围的起点,例:range.moveStart("word", 2);
,起点移动2 个单词。
moveEnd()
:移动范围的终点,例:range.moveEnd("character", 1);
,终点移动1 个字符。
expand()
方法可以将范围规范化。换句话说,expand()
方法的作用是将任何部分选择的文本全部选中。例如,当前选择的是一个单词中间的两个字符,调用expand("word")
可以将整个单词都包含在范围之内。
move()
方法则首先会折叠当前范围(让起点和终点相等),然后再将范围移动指定的单位数量。
range.move("character", 5); //移动5 个字符
调用move()
之后,范围的起点和终点相同,因此必须再使用moveStart()
或moveEnd()
创建新的选区。
操作范围
-
text
属性
通过text
属性可以取得范围中的内容文本;但是,也可以通过这个属性设置范围中的内容文本。
html:
Hello world!
js:
var range = document.body.createTextRange();
range.findText("Hello");
range.text = "Howdy";
html会变成:
Howdy world!
pasteHTML()
向范围中插入HTML
代码,但是在范围本身就包含有HTML
代码时,不建议使用。( 可能会产生格式不正确的HTML
)
html:
Hello world!
js:
var range = document.body.createTextRange();
range.findText("Hello");
range.pasteHTML("Howdy")
html会变成:
Howdy world!
-
collapse()
折叠范围
传入true
把范围折叠到起点,传入false
把范围折叠到终点。但是没有对应的collapsed
属性让我们知道范围是否已经折叠完毕。为此,必须使用boundingWidth
属性,该属性返回范围的宽度(以像素为单位)。如果boundingWidth
属性等于0,就说明范围已经折叠了。
range.collapse(true); //折叠到起点
var isCollapsed = (range.boundingWidth == 0);
// isCollapsed 为true时则已经折叠了
-
compareEndPoints()
比较范围
这个方法接受两个参数:比较的类型和要比较的范围,比较的类型也是以下几个字符串的值,"StartToStart"
、"StartToEnd"
、"EndToEnd"
和"EndToStart"
,比较方式与调用方式也与之前的compareBoundaryPoints()
方法一致。
range1.compareEndPoints("EndToEnd", range2)
-
isEqual()
比较范围是否相等 -
inRange()
用于确定一个范围是否包含另一个范围
var range1 = document.body.createTextRange();
var range2 = document.body.createTextRange();
range1.findText("Hello World");
range2.findText("Hello");
alert("range1.isEqual(range2): " + range1.isEqual(range2)); //false
alert("range1.inRange(range2):" + range1.inRange(range2)); //true
-
duplicate()
复制范围
var newRange = range.duplicate();
可以复制文本范围,结果会创建原范围的一个副本,新创建的范围会带有与原范围完全相同的属性。