解释器模式听起来高高在上,但它其实应用广泛而且非常实用,javascript是一门解释型语言,它的大多数引擎(Actionscript是一个特例)都是解释器,解释器的实现十分复杂,然而解释器模式并非如此 ,思想上解释器模式借鉴了解释器的实现,但根据需要,也可以用很简单的代码实现。
在GOF book中这样解释它的意图:给定一个语言,定义它文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。
这听起来十分复杂,但是事实是,语言可以*设定,所以它的实现可能非常简单。举个例子,假如我用wsad来代表上下左右,那么一个wasd组成的字符串即可表示一条二维空间内的路径,它的每个"句子"即是一个字母,那么实现一个根据wasd序列来显示相应路线的解释器并不困难。更进一步,可以把一个解释器看作是由字符流作为参数的复杂函数,它的复杂程度取决于字符流的格式。
解释器模式执行速度通常不快(大多数时候非常慢),而且错误调试比较困难(附注:虽然调试比较困难,但事实上它降低了错误的发生可能性),但它的优势是显而易见的,它能有效控制模块之间接口的复杂性,对于那种执行频率不高但代码频率足够高,且多样性很强的功能,解释器是非常适合的模式。此外解释器还有一个不太为人所注意的优势,就是它可以方便地跨语言和跨平台。
解释器模式在js中有两个最典型的应用json和正则表达式,对js程序员来说,这应该是很熟悉的两种东西。json用于序列化对象型数据,这个js的对象文字量形式在包括C++,Java在内的各种语言中都有实现的类库,在一些ajax应用中,java或者C#中的对象被序列化为json格式,通过相应客户端的http请求传递给客户端的js程序,js几乎不需要任何处理,仅仅使用eval就可以把json格式的数据还原成js对象(因为json恰巧是来自js),这在解释器模式的实现中是很少见的。现在,不仅仅使与js相关的应用,即使在其他语言的应用中,json也是一种很受欢迎的数据交换格式。正则表达式是js的内置对象,它可以说是最著名的解释器模式了,几乎所有语言中都有它的实现,现在它已经几乎是字符串匹配的事实标准。它能处理字符串的各种格式,有效地避免了过于复杂的string对象接口或者大段的字符串分析代码,这对开发效率至关重要。js的实现是一个比较强的版本,相比java和C#等语言,js允许函数参数为它提供了更大的灵活性。
本文将讨论如何用解释器模式解决问题以及在javascript中运用语言特性实现解释器模式。
词法分析·状态机的实现
通常解释器模式需要将所定义的"语言"字符流转换成适合的程序数据结构,再对这个结构进行分析。对于比较简单的情况,转换和分析可以在一步完成。为了很好好的完成这项工作,我们需要实现一个状态机。
状态机原本不是软件和程序中的术语,在数字逻辑中有限状态机是指输出取决于过去输入部分和当前输入部分的时序逻辑电路。这里甚至无需强调有限状态机,可以简单理解状态机为一个黑箱子,向其中投入指令后即可进行操作和装换状态,它有一个最终状态,当到达最终状态时,即可完成任务。
词法分析有限状态机任务很简单,从输入字符流中读入一个一个的字符,当辨认出输入的字符能构成一个独立的语法单元(token)时,便将这个token放入待分析的词句流中。
这里给出一个简单的例子:正斜杠转义的实现。通常字符串转义都是以反斜杠/实现的,假如有一个字符串,现在我们要把正斜杠用作转义符以做一些特殊用途,其他字符原样放置。那么正斜杠/和它后面的字符必须被看成一个整体,其它每个字符都是一个整体。
这个状态机只有两个状态 第一个状态是读入普通字符状态 第二个状态是读入正斜杠以后的状态 状态图如下
在js中 充分利用语言特性 将每个状态实现为一个函数 它接受一个状态改变参数 然后返回下一个状态
function state_machine()
{
this .state = _1;
this .resault = [];
function _1(c){
if (c != ' / ' ){
this .resault.push(c);
return _1;
} else {
return _2;
}
}
function _2(c){
this .resault.push( ' / ' + c);
return _1;
}
this .change = function (c){
this .state = this .state(c);
};
}
var sm = new state_machine();
var queue = ( " a//sd/jh/ds " ).split( ' );
for ( var i = 0 ;i < queue.length;i ++ )
sm.change(queue[i]);
alert(sm.resault);
script>
这是一个标准的状态机处理词法分析的例子,事实上,有些简单的解释器模式,仅仅通过词法分析即可实现,功能可以写在状态改变函数中,而无需对产生的token流进行处理。
函数式语言特性与状态机模式
作为函数式语言,js实现解释器模式有非常有趣的方式:以不定个数的参数形式传入函数进行处理,这样可以方便的扩展功能,同时可以使用户更*的使用解释器提供的接口。
下面一段代码是一个用于日期对象的格式化的类 它是状态机词法分析的一个稍微复杂的例子,同时它以函数参数的方式为用户提供了扩展功能。
/*
DateEx类
说明:以参数形式继承自Date对象 为Date对象扩展方法
方法:
format(formatString,[fun],......)
参数:
formatString:格式字符串 将日期转换成所规定的格式字符串
格式说明:
%[x]:
[x]代表日期的一个部分
%y:年
%m:月
%d:日
%w:星期
%h:小时
%i:分
%s:秒
%[num][x]:
[num]代表长度 [x]意义同上 如果长度不足则用0补齐 如果长度超出[num]则将高位截断
%f[x]:
以自定义函数处理%[x]得到的值,自定义函数在参数列表[fun]中给出,参数中[fun]的个数应与%f[x]的数目一致
fun:可选的,处理函数,当格式字符串中有格式符%f出现时,则在fun中取相应的函数处理
*/
function DateEx(date){
date=date||new Date();
date.format=function(formatString)
{
var f;
var j=0;
function fbuilder(n){
return function(v){
var s=v.toString();
if(s.length>=n)return s.slice(s.length-n,s.length);
if(s.length<n)return new Array(n-s.length+1).join(0)+s;
};
}
var args=arguments;
var resault=new String();
var _1=function(c)//状态1 是读入格式字符串的状态
{
if(c!="%")//对于非%字符按原样输出
{
resault+=c;
return _1;
}
else//读到%时进入状态2 否则延续状态1
{
return _2;
}
};
var _2=function(c)//状态2 是读入特殊格式字符串的状态
{
if(c.match(/d/)!=null)//对于数字 构造相应处理函数 返回状态3
{
f=fbuilder(Number(c));
return _3;
}
else if(c=="f")//对于格式符f 从参数中获取相应处理函数 返回状态3
{
f=args[++j];
return _3;
}
else//没有特殊格式符 直接进入状态3
{
f=function(v){return v;}
return _3(c);
}
};
var _3=function(c)
{
if(c=="%")//格式符% 连续2个%将被转义为一个% 返回状态1
{
resault+=c;
return _1;
}
else if(c=="y")//格式符y 取出年份 返回状态1
{
resault+=f(date.getFullYear());
return _1;
}
else if(c=="m")//格式符m 取出月份 返回状态1
{
resault+=f(date.getMonth()+1);
return _1;
}
else if(c=="d")//格式符d 取出日期 返回状态1
{
resault+=f(date.getDate());
return _1;
}
else if(c=="w")//格式符w 取出星期 返回状态1
{
resault+=f(date.getDay());
return _1;
}
else if(c=="h")//格式符h 取出小时 返回状态1
{
resault+=f(date.getHours());
return _1;
}
else if(c=="i")//格式符i 取出分 返回状态1
{
resault+=f(date.getMinutes());
return _1;
}
else if(c=="s")//格式符s 取出秒 返回状态1
{
resault+=f(date.getSeconds());
return _1;
}
else return _1//没有合法格式符 忽略 返回状态1
};
var status=_1;
for(var i=0;i<formatString.length;i++)
{
status=status(formatString.charAt(i));
}
return resault;
}
return date;
}
var weekdays="日一二三四五六"
document.write(new DateEx().format("%2y-%2m-%2d 星期%fw %2h:%2i:%2s %%",function(v){return weekdays.charAt(v);}))
动态语言特性·eval与解释器模式
js的另一个非常有趣特点是它本身是一门解释型语言,它允许用eval和Function等方式调用其本身的解释器引擎,这样给解释器的实现带来了很大的方便,可以将某段自定义语言(如代数运算或者布尔运算不分)作为一个独立的token用eval直接执行,这种形式的解释器是静态语言无法比拟的