【JS设计模式】语言翻译——解释器模式

451 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第4天,点击查看活动详情

解释器模式(Interperter): 对于一种语言,给出其文法表示形式,并定义一种解释器,通过使用这种解释器来解释语言中定义的句子。

文法

文法就是一种语言中的语法描述的工具,比如说我们汉语中的疑问句“XXXX吗?”,感叹句“XXXX啊!”,也就是说文法是用来定义一组语言规则的。

如果我们要实现一个需求:统计页面中点击事件触发元素在页面中所处的路径,就可以使用解释器模式,但是前提是我们要先定义好一种文法。

<div class="wrap">
    <div class="link-inner">
        <a href="#">link</a>
    </div>
    <div class="btn-inner">
        <button>click me</button>
    </div>
</div>

比如上面的按钮,我们将其在页面所处的路径定义为 DIV>DIV2>BUTTON,什么意思呢?第一个DIV代表的是最外层的div标签,而DIV2代表的则是,在最外层的div内部还有两个div标签,而button处于第2个div。

上面的就是我们定义的一种新的文法,而我们也用汉语对这种新文法进行了解释,因此我们现在要创建一个解释器,来实现这种文法。

解释器

在书写解释器前,我们要先对文法进行分析,提取其相似点。比如 DIV>DIV2>BUTTON 我们可以看出,最右边是我们的目标元素,而最左边则是我们的最外层容器元素,如果我们从右向左看,就会发现这个顺序有点像我们事件流中的冒泡阶段。 因此我们就可以按照事件冒泡的流程把我们元素所处页面的路径提取出来。

获取当前元素的父元素

我们可以通过 parentNode 来获取当前元素的父元素;

获取该元素前面的兄弟元素

通过 previousSibling 可以获取当前元素前的兄弟元素,考虑到前面可能不止一个兄弟元素,所以我们要对兄弟元素进行遍历,遍历时要注意的是当遍历到的这个兄弟元素的标签名称与当前元素的标签名称相同,则在当前元素名称上加一,否则的话就添加该元素的标签名称。 我们可以通过数组来保存每一层的标签名称,最后通过数组的 join 方法转换成 DIV>DIV2>BUTTON 这种形式。

先来实现遍历同级兄弟元素的方法 getSublingName

image.png

上面这个方法返回的是一个字符类型的数字,代表的是参数node在相同标签名的兄弟元素中排第几。

实现解释器

接下来我们要开始实现遍历文档树,并以此来实现我们的解释器。 我们将其称为 XPath 解释器, 因为是获取元素的XPath的。

image.png

上面的代码中有两点是需要注意的,首先是返回的匿名函数的容器节点参数,这里不能使用默认参数,也就是说不能像下面这样的写法:

return function (node,containerNode = document)

这样写的话,会导致函数内部无法使用 arguments.callee,具体产生的原因暂时还不清楚。

第二点就是有关 arguments.callee 了,它是 arguments 的一个属性,是一个指向 arguments 对象所在的函数的指针。 一般情况下,我们在都是通过在函数内部调用自己来实现递归的,如下

function getNode(node){
    // ....
    getNode(node)
}

但是这样的写法会导致函数逻辑和函数名紧密耦合。 如果我们修改了函数名,还要跑进函数体内将递归调用的函数名也修改了,这种情况我们就可以使用 arguments.callee 来实现函数逻辑和函数名解耦。

function getNode(node){
    // ...
    arguments.callee(node)
}

这样,无论函数如何改名字,都意味着递归调用它本身。

回到正文,解释器定义好之后,我们来试试效果,先书写一段HTML代码,如下:

<html>
<head></head>
<body>
    <div></div>
    <div class="container">
        <div>
            <div>
                <ul>
                    <li><span id="sp1"></span></li>
                    <li><span id="sp2"></span></li>
                </ul>
            </div>
        </div>
        <div>
            <div>
                <ul>
                    <li><span id="sp3"></span></li>
                    <li><span id="sp4"></span></li>
                </ul>
            </div>
        </div>
    </div>
</body>
</html>

假如我们要获取id为sp3的元素相对于document的路径,只需要调用解释器

const path = Interpreter(document.getElementById('sp3))
console.log(path.join('>'))   // HTML>BODY|HEAD>DIV2>DIV2>DIV>UL>LI>SPAN

小结

是否可以应用解释器模式的一条重要准则就是,能否根据需求解析出一套完整的语法规则。 不论语法规则简单还是复杂都是必须的,因为解释器都要按照这套规则才能实现相应的功能。