二叉树祖先节点
对于这个小巧的函数系列的第八篇文章,我将介绍一个名为ancestor()
的函数。 顾名思义,该函数根据标签名称和/或类匹配获得对给定节点祖先的引用。
这是ancestor()
函数的代码:
function ancestor(node, match)
{
if(!node)
{
return null;
}
else if(!node.nodeType || typeof(match) != 'string')
{
return node;
}
if((match = match.split('.')).length === 1)
{
match.push(null);
}
else if(!match[0])
{
match[0] = null;
}
do
{
if
(
(
!match[0]
||
match[0].toLowerCase() == node.nodeName.toLowerCase())
&&
(
!match[1]
||
new RegExp('( |^)(' + match[1] + ')( |$)').test(node.className)
)
)
{
break;
}
}
while(node = node.parentNode);
return node;
}
第一个参数是对原始节点的引用-可以是任何种类的DOM节点,但通常是一个元素。 第二个参数是一个字符串,用于标识祖先,可以是简单的标记名(如"ul"
)或类选择器(如".menu"
,也可以是两者的组合,如"ul.menu"
。 该函数将从原始节点向上迭代,并返回与字符串模式匹配的第一个祖先节点;如果找不到此类祖先,则返回null
。
函数的作用是什么
此功能最常见的用例是在事件处理代码中-识别事件目标中的包含元素,而不必知道其间还有其他节点。 也许我们甚至都不知道祖先是什么类型的元素。 ancestor()
函数通过针对我们拥有的所有信息迭代检查父节点来处理此问题。
例如,假设我们将focus
事件绑定到一组菜单链接,并带有需要获取对包含列表项的引用的处理程序代码。 动态菜单通常需要在它们支持的标记类型上非常灵活,不仅要考虑像这样的简单项目:
<li>
<a>...</a>
</li>
但也包括更复杂的项目,添加了用于额外语义或样式钩子的其他元素:
<li>
<h3>
<span>
<a>...</a>
</span>
</h3>
</li>
将添加JavaScript来处理链接focus
事件(由于焦点事件不会冒泡 ,因此必须单独添加):
var links = menu.getElementsByTagName('a');
for(var len = links.length, i = 0; i < len; i ++)
{
links[i].addEventListener('focus', function(e)
{
var link = e.target;
}, false);
}
然后, ancestor()
函数可以处理目标转换:
var item = ancestor(link, 'li');
第二个参数的灵活性允许不同的信息情况,例如,我们知道包含菜单将具有"menu"
class
,但我们不知道它是<ul>
还是<ol>
元素:
var menu = ancestor(link, '.menu');
或者,也许我们有一个更深层的结构,其中各个子菜单是无序列表( <ul class="menu">
),而顶层导航栏是具有相同class
名的有序列表( <ol class="menu">
)。 我们可以在匹配class
中定义标签名称和class
,以获取所需的特定引用:
var navbar = ancestor(link, 'ol.menu');
在这种情况下的话,任何数量的其他的"menu"
的元素将被忽略,如果用它既标签名称和相匹配的祖先只返回class
。
功能如何运作
基本功能只是通过DOM的向上迭代 。 我们从原始节点开始,然后检查每个parentNode
直到指定的祖先匹配为止;如果节点用完(即,如果到达#document
却找不到所需的节点),则放弃迭代。 但是,我们还有一些测试代码,以确保正确定义了两个参数:
if(!node)
{
return null;
}
else if(!node.nodeType || typeof(match) != 'string')
{
return node;
}
如果输入node
参数未定义或为null
,则函数返回null
; 或者,如果输入node
不是节点,或者输入match
不是字符串,则该函数将返回原始节点。 这些只是安全条件,通过减少对发送给它的数据进行预测试的需求,使功能更强大。
接下来,我们处理match
参数以创建两个值的数组-第一个是指定的标记名(如果未指定,则为null
),第二个是指定的类名(如果未指定,则为null
):
if((match = match.split('.')).length === 1)
{
match.push(null);
}
else if(!match[0])
{
match[0] = null;
}
最后,我们可以进行迭代检查,将每次迭代中的当前参考节点与match
数组中定义的标准进行比较。 如果match[0]
(标记名称)为null
则任何元素都将匹配,否则我们仅匹配具有指定标记名称的元素(将两者都转换为小写,因此匹配不区分大小写)。 同样,如果match[1]
(类名)为null
那么一切都很好,否则该元素必须包含指定的class
:
do
{
if
(
(
!match[0]
||
match[0].toLowerCase() == node.nodeName.toLowerCase())
&&
(
!match[1]
||
new RegExp('( |^)(' + match[1] + ')( |$)').test(node.className)
)
)
{
break;
}
}
while(node = node.parentNode);
如果两个条件都匹配,我们中断迭代,并返回当前参考节点; 否则,我们继续下一个parentNode
。 如果我们允许代码在两个match
值都为null
时达到这个目标,那么最终结果将是我们返回原始node
,这正是开始时的安全状况。
关于迭代本身有趣的事情是do...while
的使用:
do
{
...
}
while(node = node.parentNode);
在while
评估内部,我们利用了在评估内部定义分配的功能。 每次进行评估时, node
引用都将转换为parentNode
node
并重新分配。 该分配返回分配的node
。 如果父代不存在,则node
引用将为null
,因此它不会通过while
条件,因此迭代将停止并且函数将返回null
。 但是,如果父节点确实存在,它将通过while
条件,因此迭代将继续,因为任何节点引用的评估结果为true
,而null
评估结果为false
。
免费学习PHP!
全面介绍PHP和MySQL,从而实现服务器端编程的飞跃。
原价$ 11.95 您的完全免费
由于我们必须测试的节点数是未知的,因此只要父级存在,就必须使用while
语句进行迭代。 但是,通过使用do...while
而不是while
,我们可以在转换为父节点之前评估原始节点(因为do
在第一个while
之前评估)。 最终,这意味着如果原始节点已经通过匹配条件,它将立即返回,这使我们不必在迭代之前定义单独的if
条件。
结论
ancestor()
函数不会赢得任何复杂的奖励! 但是简单功能的抽象是编程的基础,提供了可重复使用的代码,从而节省了重复键入相同的基本逻辑的麻烦。
二叉树祖先节点