使用函数和闭包可以构建模块。所谓模块,就是一个提供接口却隐藏状态与实现的函数或对象。通过使用函数构建模块,可以完全摒弃全局变量的使用,从而规避JavaScript语言缺陷。全局变量是JavaScript最为糟糕的特性之一,在一个大中型Web应用中,全局变量可能会带来不可预料的后果。
例如,要为String扩展一个deentityify方法,其设计任务是寻找字符串中的HTML字符实体并将其替换为对应的字符。在一个对象中保存字符实体的名字及与之对应的字符是有意义的。
可以把deentityify放到一个全局变量中,但全局变量存在很多潜在危害。可以把deentityify定义在该函数本身中,但会带来运行时的损耗,因为在该函数每次被执行时,这个方法都会被求值一次。理想的方式是将deentityify放入一个闭包,而且也许还能提供一个增加更多字符实体的扩展方法。
String.method('deentityify',function{
var entity={
quot:'"',
lt:'<',
gt:'>'
};
return function{
return this.replace(/&([^&;]+);/g,function(a,b){
var r=entity[b];
return typeof r==='string'?r:a;
});
};
});
在上面代码中,为String类型扩展了一个deentityify方法,它调用字符串的replace方法来查找以“&”开头和以“;”结束的子字符串。如果这些字符可以在字符实体表entity中找到,那么就将该字符实体替换为映射表中的值。deentityify方法用到了一个正则表达式:
return this.replace(/&([^&;]+);/g,function(a,b){
var r=entity[b];
return typeof r==='string'?r:a;
});
在最后一行使用运算符立刻调用刚刚构造出来的函数。这个调用所创建并返回的函数才是deentityify方法。
document.writeln('<">'.deentityify);//<">
模块利用了函数作用域和闭包来创建绑定对象与私有成员的关联。在这个示例中,只有deentityify方法才有权访问字符实体表entity这个数据对象。模块开发的一般形式是:一个定义了私有变量和函数的函数,利用闭包创建可以访问到的私有变量和函数的特权函数,最后返回这个特权函数,或者把它们保存到可访问的地方。
使用模块可以避免全局变量的滥用,从而保护信息的安全性,实现优秀的设计实践。使用这种模式也可以实现应用程序的封装,或者构建其他实例对象。
模块模式通常结合实例模式使用。JavaScript的实例就是用对象字面量表示法创建的,对象的属性值可以是数值或函数,并且属性值在该对象的生命周期中不会发生变化。模块通常作为工具为程序其他部分提供功能支持。通过这种方式能够构建比较安全的对象。
下面代码构造一个用来产生序列号的对象。serial_maker函数将返回一个用来产生唯一字符串的对象,这个字符串由两部分组成:字符前缀+序列号。这两部分可以分别使用set_prefix和set_seq方法进行设置,然后调用实例对象的gensym方法读取这个字符串。当执行该方法时,都会自动产生唯一一个字符串。
var serial_maker=function{
var prefix='';
var seq=0;
return{
set_prefix:function(p){
prefix=String(p);
},
set_seq:function(s){
seq=s;
},
gensym:function{
var result=prefix+seq;
seq+=1;
return result;
}
};
};
var seqer=serial_maker;
seqer.set_prefix('Q');
seqer.set_seq(1000);
var unique=seqer.gensym;//"Q1000"
var unique=seqer.gensym;//"Q1001"
seqer包含的方法都没有用到this或that,因此没有办法“损害”seger,除非调用对应的方法,否则无法改变prefix或seq的值。由于seqer对象是可变的,所以它的方法可能会被替换掉,但替换后的方法依然不能访问私有成员。seqer就是一组函数的集合,而且这些函数被授予特权,拥有使用或修改私有状态的能力。如果把seqer.gensym作为一个值传递给第三方函数,这个函数就能通过它产生唯一字符串,却不能通过它来改变prefix或seq的值。