jQuery is DSL

jQuery刚刚出来的时候,我没有太多关注它,觉得这不过是Yet Another JavaScript Library。早期的jQuery专注于DOM节点的筛选与操作,不提供众多的基础类扩展,更不提供UI组件,因此体积能够做到很小。然而,我实在看不出它和我熟悉的Prototype比有什么明显的优势——jQuery能做的各项独立的操作,Prototype都能做。

后来用jQuery的人越来越多,并且大家都爱用它的链式方法调用,甚至还把这种写法推广到其它语言中去。例如ASP.NET MVP 就把他的服务器端C#组件设计为支持链式方法调用的。这时候我才开始关注jQuery,并且逐渐喜欢上了链式方法调用的写法,也在我自己的JavaScript组件中实现类似的API(参考和)。最后,我突然明白到,这其实就是一种Internal DSL嘛!

在这篇文章里,我准备先讨论Internal DSL,在下一篇文章里面再解释为什么jQuery是Internal DSL。现在我们就从最根本的问题开始吧——

什么是Internal DSL?

DSL是指Domain Specific Language,也就是用于描述和解决特定领域问题的语言。例如说,我们有专门描述字符串特征的正则表达式,有专门描述数据库查询的SQL,有专门描述XML结构的DTD和XSD,甚至有专门描述XML变换的XSLT,这些都是DSL。

当然,并非我们关注的领域都有现成的DSL,这时候我们有三个选择:

使用通用语言描述该领域的问题(non-DSL)

发明一门全新的语言描述该领域的问题()

在一门现成语言内实现针对领域问题的描述()

例如说,我们现在要描述一个很简单的金融领域问题,“我在花旗银行存款$200”这样一句话对应的三种法写法可能是:(假设已经存在I和CitiBank两个实体实例)

I.DepositTo(new USD(200), CitiBank); /* C# */

I deposit 200USD to CitiBank /* E-DSL */

I.deposit(200.USD()).to(CitiBank); /* I-DSL */

第1种做法的成本最低,你只需要有OO的思想就可以了,你总能把实体类设计出来,但可能和人类描述此领域问题的思维方式有一定偏差(为什么USD可以new?为什么不是deposit [something] to [somewhere]?)。

 

第2种做法的成本最高,你需要写一个全新的解释器,至少是写一组全新的规则,然后让YACC这类工具帮你生成一个解释器,但这样出来的语法最贴近人类思维方式,甚至就如同自然语言一样流畅。

第3种做法术语上述两者的折中方案,如果语法不太复杂可以使用Builder模式实现语法分析,写出来的语法相当贴近自然语言,但还是有学习门槛。由于脚本语言有相当的灵活性,所以现在很多人倾向于选择在脚本语言内实现Internal DSL。

如何构造Internal DSL?

常见的两种Internal DSL实现方法是和。如果我们需要描述一台机器的硬件组成,两种实现方式的代码分别如下:

/* Method Chaining */
computer()
  .processor()
    .cores(2)
    .i386()
  .disk()
    .size(150)
  .disk()
    .size(75)
    .speed(7200)
    .sata()
  .end();

/* Function Sequence */
computer();
  processor();
    cores(2);
    processorType(i386);
  disk();
    diskSize(150);
  disk();
    diskSize(75);
    diskSpeed(7200);
    diskInterface(SATA);

无论是哪一种写法,中间都必须写一个分析器层。就如同语法分析器需要使用状态机一样,Internal DSL的实现也必须内置一个状态机,以记录当前执行到什么状态了,并且接下来可以转移到哪些有效状态。

由于这不是一篇专门讲语法分析器和状态机实现的文章,所以我们把关注点保持在API层面就可以了,不深入讨论其实现细节和成本。我们知道链式方法调用能够实现Internal DSL就够了,至于jQuery是如何利用好这一点的,我们在下一篇文章里再作讨论。

小结

在这篇文章里,我们了解了Internal DSL与External DSL之间的区别,同时还了解到实现Internal DSL的具体方式,这为我们接下来讨论jQuery的Internal DSL式接口做好了铺垫。在下一篇文章里,我们将深入地来看看为什么jQuery的接口要如此设计,它能为用户带来了怎样的便利,同时它自身的实现上又有什么优势。

 

jQuery的Internal DSL形式

在上一篇文章里面,我们了解到了Internal DSL的具体形式,形如:

/* Method Chaining */
computer()
  .processor()
    .cores(2)
    .i386()
  .disk()
    .size(150)
  .disk()
    .size(75)
    .speed(7200)
    .sata()
  .end();

然后我们在看看一段典型的jQuery代码:

$("ul#contacts li.item")
  .find("span.name")
    .click(function(e) { $(e.target).siblings(".more").toggle(); })
    .end()
  .find("input.delete")
    .click(function(e) { $(e.target).parents(".item").remove(); })
    .end()
  .find("div.more")
    .hide()
    .end();

从结构上来说,是不是跟上面那一段Internal DSL的例子很相似?就算我们不看对应的HTML,我们也能猜到这段jQuery代码的含义:

遍历

  • 中的每一个


  • (这看起来是个联系人列表)


隐藏这个div
(默认隐藏详细信息?)

绑定click事件,操作是把class="item"父节点删除
(这应该是用来删除联系人的)

绑定click事件,操作是显示/隐藏class="more"兄弟节点
(这是估计联系人姓名,点击后切换详细信息的显示/隐藏)

对于里面的

对于里面的

对于里面的

从这里我们已经能够看出jQuery的Internal DSL形式带来的好处——编写代码时,让代码更贴近作者的思维模式;阅读代码时,让读者更容易理解代码的含义。不信?我们看看与jQuery拥有相似功能的Prototype是如何实现上述逻辑:

$$("ul#contacts li.item span.name")
  .invoke("observe", "click",
    function(e) { $(e.target).next(".more").toggle(); });
$$("ul#contacts li.item input.delete")
  .invoke("observe", "click",
    function(e) { $(e.target).up(".item").remove(); });
$$("ul#contacts li.item div.more")
  .invoke("hide");

这是我用Prototype所能写出的最贴近Internal DSL的形式了。(如果你能够写出一个更自然的版本,欢迎分享。)在Prototype里面,能够返回一组元素的操作就只有$$(),并且它只能作用于全局,缺乏jQuery中find()或者filter()的功能,所以这一组描述联系人列表行为的语句无法组合在一起,必须逐一定义每类元素的行为。此外,此例子中每类元素都仅仅指定了一个行为,因此Prototype的invoke()写法看起来还是和jQuery的click()写法很相近的。但如果一类元素拥有多个行为,Prototype的invoke()就不能好像jQuery那样链式调用下去了,必须每一个行为重头写一个$$(),或者把invoke()改成each()加匿名函数。无论是那种做法,都只会降低代码的可读性。

jQuery的语法分析器

我们都知道,Internal DSL的实现依赖于对语法分析器的封装,对Internal DSL的调用其实都是对语法分析器的调用,经过语法分析后再构造出对底层API的调用。例如jQuery当中的click(),它依赖于当前的状态,也就是前面$()筛选出来的节点集合,把click()解释为要为这一组节点绑定DOM的click事件,最后再调用DOM API完成任务。在这个例子当中,DOM API相对jQuery API而言就是底层API了。

 

jQuery可以说是挑了一个最容易实现的语法模型来做,永远只有一种token,因此永远也只有一种状态,这种状态当然也是永远有效的,你根本不可能给jQuery输入一个当前状态无效的token。jQuery的唯一状态就是一个jQuery对象实例,其本质就是一个元素集合。读入的token可能是各种针对这个元素集合的操作,但它的返回一定还是一个元素集合。这使得jQuery的语法分析器不会进入无效状态,也就无需判断无效状态,因此大大简化了Internal DSL实现中常见的一个难题。

小结

通过拿jQuery和Prototype做对比,我们可以发现jQuery用非常低的成本实现了Internal DSL,同时带来了Prototype所没有的明显好处。这可以看作是一个很好的范例——如果你需要描述的业务逻辑能够归纳为简单的语言模式,为此实现一门Internal DSL的性价比将会是很高的。你需要做的仅仅是为这个简单的语言模型实现一个简单的解释器,接着你就可以享受贴近人类思维模式的接口了。


来源:Cat Chen - 博客园

上一篇: 巧妙使用checkbox制作纯css动态导航栏程序媛_Mickey

下一篇: JavaScript中的陷阱大集合

分享到: 更多