DOM2 和 DOM3
遍历
- DOM Level 2 Tranversal and Range 模块定义了2个用于辅助完成顺序遍历DOM结构的类型:NodeIterator和TreeWalker。这两个类型能够基于给定的起点对DOM结构执行深度优先的遍历操作。IE不支持DOM遍历(然而IE9+却支持,我遇到书中好多地方说IE不支持,难道这部分的知识的编写时期还没有出来IE9?)。使用下面代码可以检测浏览器对DOM2 级遍历能力的支持情况:
var supportsTraversals = document.implementation.hasFeature("Traversal", "2.0");
var supportsNodeIterator = (typeof document.createNodeIterator == "function");
var supportsTreeWalker = (typeof document.createTreeWalker == "function");
alert(supportsTraversals);//IE 8- false
alert(supportsNodeIterator);//IE 8- false
alert(supportsTreeWalker);//IE 8- false
NodeIterator
- 可以使用document.createNodeIterator()方法创建新实例,这个方法接收4个参数:
- root:搜索起点
- whatToShow:表示要访问哪些类型节点的数字代码
- filter:是一个NodeFilter对象,或者一个表示过滤哪些特殊节点的函数。
- entityReferenceExpansion:布尔值,表示是否要扩展实体引用。这个参数在HTML中没有用,因为其中的实体引用不能扩展。
- 其中whatToShow是一个位掩码,通过应用一或多个过滤器来确定要访问哪些节点。这个参数的值以常量形式在NodeFilter(IE8-不支持)类型中定义,如下所示:
访问方式 | 值 | 描述 |
---|---|---|
NodeFilter.SHOW_ALL | 4294967295(32个1) | 显示所有类型的节点 |
NodeFilter.SHOW_ELEMENT | 1(第一个1) | 显示元素节点 |
NodeFilter.SHOW_ATTRIBUTE | 2(第二个个1) | 显示特性节点,由于DOM结构原因,实际上不能使用这个值,用了也没效果。 |
NodeFilter.SHOW_TEXT | 4 | 显示文本节点 |
NodeFilter.SHOW_CDATA_SECTION | 8 | 显示CDATA节点。对HTML无效(因为没有这样的节点,在XML中存在) |
NodeFilter.SHOW_ENTITY_REFERENCE | 16 | 显示实体引用节点。对HTML无效 |
NodeFilter.SHOW_ENTITY | 32 | 显示实体节点。对HTML无效 |
NodeFilter.SHOW_PROCESSING_INSTRUCTION | 64 | 显示处理指令节点。对HTML无效 |
NodeFilter.SHOW_COMMENT | 128 | 显示注释节点 |
NodeFilter.SHOW_DOCUMENT | 256 | 显示文档节点 |
NodeFilter.SHOW_DOCUMENT_TYPE | 512 | 显示文档类型节点 |
NodeFilter.SHOW_DOCUMENT_FRAGMENT | 1024 | 显示文档片段节点。对HTML无效 |
NodeFilter.SHOW_NOTATION | 2048 | 显示符号节点。对HTML无效 |
- 一般我们不会在意具体的值是多少,比如我们只显示元素和文本节点,可以使用:
NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT
作为第二个参数。另外可以发现每个参量代表的具体的值是2^(nodeType - 1)
。
console.log(NodeFilter.SHOW_ALL);//4294967295 32个1
console.log(NodeFilter.SHOW_ELEMENT);//1 2^(1-1)
console.log(NodeFilter.SHOW_ATTRIBUTE);//2 2^(2-1)
console.log(NodeFilter.SHOW_TEXT);//4 2^(3-1)
console.log(NodeFilter.SHOW_CDATA_SECTION);//8 2^(4-1)
console.log(NodeFilter.SHOW_ENTITY_REFERENCE);//16 2^(5-1)
console.log(NodeFilter.SHOW_ENTITY);//32 2^(6-1)
console.log(NodeFilter.SHOW_PROCESSING_INSTRUCTION);//64
console.log(NodeFilter.SHOW_COMMENT);//128
console.log(NodeFilter.SHOW_DOCUMENT);//256
console.log(NodeFilter.SHOW_DOCUMENT_TYPE);//512
console.log(NodeFilter.SHOW_DOCUMENT_FRAGMENT);//1024
console.log(NodeFilter.SHOW_NOTATION);//2048 2^(12-1)
- filter参数可以是一个NodeFilter对象,也可以是一个函数。如果是前者,每个NodeFilter对象只有一个一个方法,即acceptNode(),我们需要设置这个方法。该方法有3个返回值(可以查看我上面的快捷链接的介绍):FILTER_ACCEPT、FILTER_REJECT和FILTER_SKIP(他们的值分别是1、2、3)。对于createNodeIterator()来说,如果应该返回给定的节点,则acceptNode()需要返回FILTER_ACCEPT否则就返回FILTER_SKIP(也可以使用FILTER_REJECT,效果一样)。见下面的例子:
var filter = {
acceptNode: function (node) {
return node.tagName.toLowerCase() == "p" ?
NodeFilter.FILTER_ACCEPT:
NodeFilter.FILTER_SKIP;
}
};
//几乎没有区别,也需要返回NodeFilter.FILTER_ACCEPT或者NodeFilter.FILTER_SKIP
var filter2 = function (node) {
return node.tagName.toLowerCase() == "p" ?
NodeFilter.FILTER_ACCEPT:
NodeFilter.FILTER_SKIP;
}
- NodeIterator类型的主要两个方法是nextNode()和previousNode()。看看名字就知道是什么用处,直接上例子:
<!DOCTYPE html>
<html>
<head>
<title>NodeIterator Example</title>
<script type="text/javascript">
var filter = function (node) {
return node.tagName.toLowerCase() == "li"?
NodeFilter.FILTER_ACCEPT:
NodeFilter.FILTER_SKIP;
};
function makeList() {
var div = document.getElementById("div1");
var iterator = document.createNodeIterator(div, NodeFilter.SHOW_ELEMENT, filter, false);
var output = document.getElementById("text1");
output.value = "";
//这种获取行为是动态的,nextNode()只会返回最新状态,而不是createNodeIterator时的状态
var node = iterator.nextNode();
while (node !== null) {
output.value += node.tagName + "\n";
node = iterator.nextNode();
}
}
</script>
</head>
<body>
<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>
<textarea rows="10" cols="40" id="text1"></textarea><br />
<input type="button" value="Make List" onclick="makeList()" />
<input type="button" value="changeFilter" onclick="filter = null;" />
</body>
</html>
- 虽然说挺强大的,但是我觉得用处可能没有那么广,因为我们通常只会遍历一层子节点,所以通过childNodes,再通过nextSibling和previousSibling可能会更实用一点。这个方法更多用到’深入骨髓’的那种遍历,比如说我要获取文档下的a节点和div节点的集合。虽然他们各自可以通过getElementsByTagName获得,但是要按照顺序获得他们的集合,就没有那么容易写了,下面是我想到的最普通的深度优先遍历的写法。
<html>
<body>
<div id="test">
<a href="#"></a>
<div>
<a href="#"></a>
</div>
<h1></h1>
<h2></h2>
<div>
<div>
<a href="#"></a>
</div>
<a href="#"></a>
</div>
</div>
<input type="button" value="test" onclick="console.log(getElements(document.getElementById('test'),filter));">
<script>
function filter(node) {
//我随便写了一个过滤器。
var name = node.tagName.toLowerCase();
return name == "a" || name == "div";
}
//自定义的一种遍历,我决定用递归去实现。
function getElements(root, filter) {
var arr = [];//用数组保存,说明我这个方法的缺点是非动态的。
if (root) {
if (filter(root)) {
arr.push(root);
}
var children = root.children;//这里用children不用childNodes是为了过滤文本节点。只是测试
for (var i=0, len=children.length; i<len; i++) {
arr.push.apply(arr, getElements(children[i], filter));
}
}
return arr;
}
</script>
</body>
</html>
TreeWalker
- TreeWalker 是NodeIterator的一个更高级的版本。除了包括nextNode()和previousNode()在内的相同功能外,这个类型还提供了用于不同方向上遍历DOM结构的方法。
- parentNode():遍历到当前节点的父节点。
- firstChild():遍历到当前节点的第一个子节点。
- lastChild():遍历到当前节点的最后一个子节点。
- nextSibling():遍历到当前节点的下一个兄弟节点。
- previousSibling():遍历到当前节点的上一个兄弟节点。
- 创建TreeWalker对象要使用document.createTreeWalker()方法,这个方法同样接收4个参数。用法和document.createNodeIterator()类似。还记得前面说到的filter参数的返回值吗?前面只提到了FILTER_ACCEPT和FILTER_SKIP,至于FILTER_REJECT的用法在createNodeIterator()中与FILTER_SKIP相同,但是在createTreeWalker()中,则会跳过相应节点及该节点的整个子树。
- TreeWalker类型还有一个属性,名叫currentNode。顾名思义,表示任何遍历方法在上一次遍历中返回的节点。通过设置这个属性也可以修改遍历继续进行的起点,例子如下:
var node = walker.nextNode();
alert(node === walker.currentNode);//true
walker.currentNode = document.body;//修改起点
- 这种修改遍历起点的能力引起了我的兴趣。假设我修改的节点并不是一开始root的子节点,那么是否还会遍历修改后的节点的兄弟节点?如果是子节点又会如何?接下来做一个实验:
<html>
<body>
<div id="father">
<div id="test1">
<div id="test11">
<div id="test111"></div>
<div id="test112"></div>
</div>
<div id="test12"></div>
</div>
<div id="test2">
<div id="test21"></div>
<div id="test22"></div>
</div>
<div id="test3">
<div id="test31"></div>
<div id="test32"></div>
</div>
</div>
<div id="brother"></div>
<script>
var node = document.getElementById("test1");
var walker = document.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);
var currentNode = walker.nextNode();
while (currentNode != null) {
if (currentNode.tagName.toLowerCase() == "script") {
console.log("script!");
} else {
console.log(currentNode.id);
}
if (currentNode.id == "test111") {
walker.currentNode = document.getElementById("test2");
currentNode = walker.currentNode;
continue;
}
currentNode = walker.nextNode();
}
</script>
</body>
</html>
运行结果如下:
-----------------------------------
test11
test111
test2
test21
test22
test3
test31
test32
brother
script!
- 通过结果可以看出,即使一开始遍历的根节点是test1,在修改了currentNode后,会认为是从当前文档下已经遍历到修改后的节点,继续遍历会认为是当前文档下遍历的延续。不过这只是我的猜测,我又进行了下面的测试:
<html>
<body>
<div id="father">
<div id="test1">
<div id="test11">
<div id="test111"></div>
<div id="test112"></div>
</div>
<div id="test12"></div>
</div>
<div id="test2">
<div id="test21"></div>
<div id="test22"></div>
</div>
<div id="test3">
<div id="test31"></div>
<div id="test32"></div>
</div>
</div>
<div id="brother"></div>
<script>
var node = document.getElementById("test1");
var walker = document.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);
var currentNode = walker.currentNode;//即root节点
var flag = true;
while (currentNode != null) {
if (currentNode.tagName.toLowerCase() == "script") {
console.log("script!");
} else {
console.log(currentNode.id);
}
if (currentNode.id == "test111" && flag) {
walker.currentNode = document.getElementById("test2");
currentNode = walker.currentNode;
flag = false;
continue;
}
if (flag) currentNode = walker.nextNode();
else currentNode = walker.previousNode();
}
</script>
</body>
</html>
结果如下:
--------------------------------------
test1
test11
test111
test2
test12
test112
test111
test11
test1
- 这个例子在遍历到test111节点后,立马将当前节点改为test2,且向前遍历,这个结果就很有意思了。按照我的猜测他应该会把father节点也一同遍历到,但结果并不是如此,难道只会遍历到第一个兄弟节点?
<div id="father">
<div id="test0">
<div id="test01"></div>
<div id="test02"></div>
</div>
<div id="test1">
<div id="test11">
<div id="test111"></div>
<div id="test112"></div>
</div>
<div id="test12"></div>
</div>
<div id="test2">
<div id="test21"></div>
<div id="test22"></div>
</div>
<div id="test3">
<div id="test31"></div>
<div id="test32"></div>
</div>
</div>
但是结果没有变化。。。
-----------------------------------
test1
test11
test111
test2
test12
test112
test111
test11
test1
- 后来我修改了一下函数,从test2开始遍历,搜素到test22就将test3设为当前节点,结果显示,最终只会向前遍历到test2节点。可见向前遍历只会遍历到一开始设置的root节点。那就有意思了,我在想如果我最开始的例子是先遍历test3再跳到test2向前或向后遍历,结果是不是截然相反?
<html>
<body>
<div id="father">
<div id="test0">
<div id="test01"></div>
<div id="test02"></div>
</div>
<div id="test1">
<div id="test11">
<div id="test111"></div>
<div id="test112"></div>
</div>
<div id="test12"></div>
</div>
<div id="test2">
<div id="test21"></div>
<div id="test22"></div>
</div>
<div id="test3">
<div id="test31"></div>
<div id="test32"></div>
</div>
</div>
<div id="brother"></div>
<script>
var node = document.getElementById("test3");
var walker = document.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);
var currentNode = walker.currentNode;//即root节点
var flag = true;
while (currentNode != null) {
if (currentNode.tagName.toLowerCase() == "script") {
console.log("script!");
} else {
console.log(currentNode.id);
}
if (currentNode.id == "test32" && flag) {
walker.currentNode = document.getElementById("test2");
currentNode = walker.currentNode;
flag = false;
continue;
}
if (flag) currentNode = walker.nextNode();
else currentNode = walker.previousNode();
}
</script>
</body>
</html>
- 结果真如我所想,不仅连father输出来了,就连body head html都出来了(只不过没有id没有打印)。我再做一个向后的遍历看是不是到test3就结束了:
var node = document.getElementById("test3");
var walker = document.createTreeWalker(node, NodeFilter.SHOW_ELEMENT);
var currentNode = walker.currentNode;//即root节点
var flag = true;
while (currentNode != null) {
if (currentNode.tagName.toLowerCase() == "script") {
console.log("script!");
} else {
console.log(currentNode.id);
}
if (currentNode.id == "test32" && flag) {
walker.currentNode = document.getElementById("test2");
currentNode = walker.currentNode;
flag = false;
continue;
}
currentNode = walker.nextNode();
}
结果也的确到test32就终止了。那我总结一下是这样的:当改变currentNode后遍历又遇到了之前createTreeWalker的root节点,则又会像一开始一样进行工作,否则就会以document为根节点反映一些结果。不过我的这个总结还需要靠一个例子去证明。因为之前只用到了nextNode()和previousNode()。而没有用他的其他方法。所以我决定再做几个实验:
- 检验nextSibling()(previousSibling()类似):设置test0 1 2 3一共4个兄弟节点,一开始用test2创建walker。遍历完test2跳到test0节点,然后去搜索nextSibling(),看是否能搜索到test3
- 检验parentNode():设置test0 1 2 3一共4个兄弟节点,一开始用test2创建walker。遍历完test2跳到test0节点,遍历完test0后回到test21请求parentNode()看是否会搜索到father
代码我就不给了,第一个结果的确是到test2就终止了。第二个的结果也是如我所愿遍历到test2也终止了。可见我的猜想的确是正确的。不过前面还忘记说一件事。如果修改当前节点仍然是一开始指定的root的子节点,那么除了改变当前节点位置外对遍历结果不会有任何影响。这一点从刚才的实验也可以反映。因为你哪怕改到外面的节点去了,最终要是又回到了一开始设置的节点集里,又会回归“正常”。