我们可以使用函数和闭包来构造模块。模块是一个提供接口却隐藏状态与实现的函数或对象。通过使用函数产生模块,我们几乎可以完全摒弃全局变量的使用,从而缓解这个Javascript的最为糟糕的特性之一所带来的影响。
举例来说,假定我们想要给String增加一个deentityify方法。他的任务是寻找字符串中的HTML字符实体并把它们替换为对应的字符。这就需要在一个对象中保存字符实体的名字和它们对应的字符。但我们该在哪里保存这个对象呢?我们可以把它放到一个全局变量中,但全局变量是魔鬼。我们可以把它定义在该函数的内部,但是那会带来运行时的损耗,因为每次执行该函数的时候字面量都会被求值一次。理想的方式是把它放入一个闭包,而且也许还能提供一个增加更多字符实体的扩展方法:
<script> //给所有的函数对象添加一个添加新方法的函数 Function.prototype.method = function(name,func){ if(!this.prototype[name]){ this.prototype[name] = func; } return this; } String.method('deentityify',function(){ //字符实体表。它映射字符实体的名字到对应的字符。 var entity = { quot: '"', lt: '<', gt: '>' }; //返回deentityify方法。 return function() { //这才是deentityify方法。它调用字符串的replace方法查找'&'开头和';'结尾的子字符串。如果这些字符可以在字符实体表中找到,那么就将该字符实体替换成为映射表中的值。它用到了一个正则表达式 return this.replace(/&([^&;]+);/g, function (a,b){ var r = entity[b]; return typeof r === 'string' ? r : a ; } ); }; }()); //请注意最后一行。我们用()运算法立即调用我们刚刚构造出来的函数。这个调用所创建并返回的函数才是deentityify方法 document.writeln( '<">'.deentityify()); // <;> </script>
模块模式利用了函数作用域和闭包来创建被绑定对象与私有成员的关联,在这个例子中,只有deentityify方法有权访问字符实体表这个数据对象。
模块模式的一般形式是:一个定义了私有变量和函数的函数;利用闭包创建可以访问私有变量和函数的特权函数;最后返回这个特权函数,或者把他们保存到一个可以访问的地方。
使用模块模式就可以摒弃全局变量的使用。它促进了信息的隐藏和其他优秀的设计实践。对于引用程序的封装,或者构造其他单例对象,模块非常有效。
模块模式也可以用来生产安全对象。假定我们想要构造一个用来产生序列号的对象:
<script> var serial_marker = function () { var prefix = ''; var seq = ''; //返回一个用来产生唯一字符串的对象。 //唯一字符串由两部分组成,前缀+序列号。 //该对象包含一个设置前缀的方法,一个设置序列号的方法,和一个产生唯一字符串的gensym方法。 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_marker (); seqer.set_prefix('Q'); seqer.set_seq(1000); var unique = seqer.gensym(); console.log(unique); </script>
seqer包含的方法都没有用到this或that,因此没有办法损害seqer。除非调用对应的方法,否则没法改变prefix或seq的值。seqer对象是可变的,所以他的方法可能会被替换掉,但替换后的方法任然不能访问私有成员。seqer就是一组函数的集合,而且那些函数被授予特权,拥有使用或修改私有状态的能力。
如果我们把seqer.gensym作为一个值传递给第三方函数,那个函数能用它产生唯一字符串,但却不能通过它改变prefix或seq的值。
以上内容摘自Javascript语言精粹