深入理解 JavaScript 模块模式

 
作者:Ben Cherry
译者:傅小黑
 
模块模式是JavaScript一种常用的编码模式。这是一般的理解,但也有一些高级应用没有得到很多关注。在本文中,我将回顾基础知识,浏览一些不错的高级技巧,甚至我认为是原生基础的。
 
 
基础知识
 
首先我们开始简单概述模型模式。三年前Eric Miraglia(YUI)的博文使模型模式众所周知。如果你已经很熟悉模型模式,可以直接阅读“高级模式”。
 
 
匿名闭包
 
这是一切成为可能的基础,也是JavaScript最好的特性。我们将简单的创建匿名函数,并立即执行。所有函数内部代码都在闭包(closure)内。它提供了整个应用生命周期的私有状态
 
  1. (function () { 
  2.     // ... all vars and functions are in this scope only 
  3.     // still maintains access to all globals 
  4. }()); 
 
注意匿名函数周围的()。这是语言的要求。关键字function一般认为是函数声明,包括()就是函数表达式。
 
 
引入全局
 
JavaScript有个特性,称为隐性全局。使用变量名称时,解释器会从作用域向后寻找变量声明。如果没找到,变量会被假定入全局(以后可以全局调用)。如果会被分配使用,在还不存在时全局创建它。这意味着在匿名函数里使用全局变量很简单。不幸的是,这会导致代码难以管理,文件中不容易区分(对人而言)哪个变量是全局的。
 
幸好,匿名函数还有一个不错的选择。全局变量作为参数传递给匿名函数。将它们引入我们的代码中,既更清晰,又比使用隐性全局更快。下面是一个例子:
 
  1. (function ($, YAHOO) { 
  2.     // 当前域有权限访问全局jQuery($)和YAHOO 
  3. }(jQuery, YAHOO)); 

 
模块出口
 
有时你不只想用全局变量,但你需要先声明他们(模块的全局调用)。我们用匿名函数的返回值,很容易输出他们。这样做就完成了基本的模块模式。以下是一个完整例子:
 
  1. var MODULE = (function () { 
  2.     var my = {}, 
  3.         privateVariable = 1; 
  4.  
  5.     function privateMethod() { 
  6.         // ... 
  7.     } 
  8.  
  9.     my.moduleProperty = 1; 
  10.     my.moduleMethod = function () { 
  11.         // ... 
  12.     }; 
  13.  
  14.     return my; 
  15. }()); 
 
注意,我们声明了一个全局模块MODULE,有两个公开属性:方法MODULE.moduleMethod和属性MODULE.moduleProperty。而且,匿名函数的闭包还维持了私有内部状态。同时学会之上的内容,我们就很容易引入需要的全局变量,和输出到全局变量。
 
 
高级模式
 
对许多用户而言以上的还不足,我们可以采用以下的模式创造强大的,可扩展的结构。让我们使用MODULE模块,一个一个继续。
 
 
扩充
 
模块模式的一个限制是整个模块必须在一个文件里。任何人都了解长代码分割到不同文件的必要。还好,我们有很好的办法扩充模块。(在扩充文件)首先我们引入模块(从全局),给他添加属性,再输出他。下面是一个例子扩充模块:
 
  1. var MODULE = (function (my) { 
  2.     my.anotherMethod = function () { 
  3.         // 此前的MODULE返回my对象作为全局输出,因此这个匿名函数的参数MODULE就是上面MODULE匿名函数里的my 
  4.     }; 
  5.  
  6.     return my; 
  7. }(MODULE)); 
 
我们再次使用var关键字以保持一致性,虽然其实没必要。代码执行后,模块获得一个新公开方法MODULE.anotherMethod。扩充文件没有影响模块的私有内部状态。
 
 
松耦合扩充
 
上面的例子需要我们首先创建模块,然后扩充它,这并不总是必要的。提升JavaScript应用性能最好的操作就是异步加载脚本。因而我们可以创建灵活多部分的模块,可以将他们无顺序加载,以松耦合扩充。每个文件应有如下的结构:
 
  1. var MODULE = (function (my) { 
  2.     // add capabilities... 
  3.  
  4.     return my; 
  5. }(MODULE || {})); 
 
这个模式里,var语句是必须的,以标记引入时不存在会创建。这意味着你可以像LABjs一样同时加载所有模块文件而不被阻塞。
 
 
紧耦合扩充
 
虽然松耦合很不错,但模块上也有些限制。最重要的,你不能安全的覆写模块属性(因为没有加载顺序)。初始化时也无法使用其他文件定义的模块属性(但你可以在初始化后运行)。紧耦合扩充意味着一组加载顺序,但是允许覆写。下面是一个例子(扩充最初定义的MODULE):
 
  1. var MODULE = (function (my) { 
  2.     var old_moduleMethod = my.moduleMethod; 
  3.  
  4.     my.moduleMethod = function () { 
  5.         // method override, has access to old through old_moduleMethod... 
  6.     }; 
  7.  
  8.     return my; 
  9. }(MODULE)); 
 
我们覆写的MODULE.moduleMethod,但依旧保持着私有内部状态。
 
 
克隆和继承
 
  1. var MODULE_TWO = (function (old) { 
  2.     var my = {}, 
  3.         key; 
  4.  
  5.     for (key in old) { 
  6.         if (old.hasOwnProperty(key)) { 
  7.             my[key] = old[key]; 
  8.         } 
  9.     } 
  10.  
  11.     var super_moduleMethod = old.moduleMethod; 
  12.     my.moduleMethod = function () { 
  13.         // override method on the clone, access to super through super_moduleMethod 
  14.     }; 
  15.  
  16.     return my; 
  17. }(MODULE)); 
 
这种方式也许最不灵活。他可以实现巧妙的组合,但是牺牲了灵活性。正如我写的,对象的属性或方法不是拷贝,而是一个对象的两个引用。修改一个会影响其他。这可能可以保持递归克隆对象的属性固定,但无法固定方法,除了带eval的方法。不过,我已经完整的包含了模块。(其实就是做了一次浅拷贝)。
 
 
跨文件私有状态
 
一个模块分割成几个文件有一个严重缺陷。每个文件都有自身的私有状态,且无权访问别的文件的私有状态。这可以修复的。下面是一个松耦合扩充的例子,不同扩充文件之间保持了私有状态
 
  1. var MODULE = (function (my) { 
  2.     var _private = my._private = my._private || {}, 
  3.         _seal = my._seal = my._seal || function () { 
  4.             delete my._private; 
  5.             delete my._seal; 
  6.             delete my._unseal; 
  7.         },//模块加载后,调用以移除对_private的访问权限 
  8.         _unseal = my._unseal = my._unseal || function () { 
  9.             my._private = _private; 
  10.             my._seal = _seal; 
  11.             my._unseal = _unseal; 
  12.         };//模块加载前,开启对_private的访问,以实现扩充部分对私有内容的操作 
  13.  
  14.     // permanent access to _private, _seal, and _unseal 
  15.  
  16.     return my; 
  17. }(MODULE || {})); 
 
任何文件都可以在本地的变量_private中设置属性,他会对别的扩充立即生效(即初始化时所有扩充的私有状态都保存在_private变量,并被my._private输出)。模块完全加载了,应用调用MODULE._seal()方法阻止对私有属性的读取(干掉my._private输出)。如果此后模块又需要扩充,带有一个私有方法。加载扩充文件前调用MODULE._unseal()方法(恢复my._private,外部恢复操作权限)。加载后调用再seal()。
 
这个模式一直随我工作至今,我还没看到别的地方这样做的。我觉得这个模式很有用,值得写上。
 
 
子模块
 
最后的高级模式实际上最简单。有很多好方法创建子模块。和创建父模块是一样的:
 
  1. MODULE.sub = (function () { 
  2.     var my = {}; 
  3.     // 就是多一级命名空间 
  4.  
  5.     return my; 
  6. }()); 
 
虽然很简单,但我还是提一下。子模块有所有正常模块的功能,包括扩充和私有状态。
 
 
总结
 
大多数高级模式可以互相组合成更多有用的模式。如果要我提出一个复杂应用的设计模式,我会组合松耦合私有状态子模块
 
这里我还没有涉及性能,不过我有个小建议:模块模式是性能增益的。他简化了许多,加快代码下载。松耦合可以无阻塞并行下载,等价于提高下载速度。可能初始化比别的方法慢一点,但值得权衡。只要全局正确的引入,运行性能不会有任何损失,可能还因为局部变量和更少的引用,加快子模块的加载。
 
最后,一个例子动态加载子模块到父模块(动态创建)中。这里就不用私有状态了,其实加上也很简单。这段代码允许整个复杂分成的代码核心及其子模块等平行加载完全。
 
  1. var UTIL = (function (parent, $) { 
  2.     var my = parent.ajax = parent.ajax || {}; 
  3.  
  4.     my.get = function (url, params, callback) { 
  5.         // ok, so I'm cheating a bit :) 
  6.         return $.getJSON(url, params, callback); 
  7.     }; 
  8.  
  9.     // etc... 
  10.  
  11.     return parent; 
  12. }(UTIL || {}, jQuery)); 
 
我希望你能受益的,并请发表评论,分享您的想法。 现在,继续前进,并写出更好的,更模块化的JavaScript!
 
 
后记
 
1、高级里的内容,我怎么没想过呢,差距啊TT
2、此文来自于OSC之前推出的翻译板块,双手双脚的顶啊,母语阅读起来迅速多了。
3、附件是整理出的的此文的Markdown文件(上传md后缀非法TT)。
   ps:Markdown或reStructuredText,51cto能支持就好了^^