模块模式是一个常用的JavaScript编程模式。它很好理解,但是还有一些高级的使用方法没有引起广泛的注意。如果你已经非常了解模块模式,可以跳到"高级模式"的段落。
51CTO推荐阅读:JavaScript中的函数式编程实践
匿名闭包
匿名闭包是让一切成为可能的基础,而且这也是JavaScript最好的特性。我们创建个简单的匿名函数看看。函数内运行的代码都存在于闭包内,这个闭包在整个应用的生命周期内都保持私密和自己的状态。(相关阅读:揭开Javascript闭包的真实面目)
- (function () {
- // 所有的var和function都只存在于当前作用域内
- // 仍然可以读取到所有的全局变量
- }());
注意:包住匿名函数的"()"。这是JavaScript语言本身的要求,因为由function开头的代码一律被识别为"函数声明",用()包住则创建了一个函数表达式。
引用全局变量
JavaScript有个特质隐含的全局变量。无论一个name是否使用过,JavaScript解释器反向遍历作用域链查找这个name的var声明,如果没找到var,则这个对象是全局的。这意味着在一个匿名闭包中使用和创建全局变量很容易。不幸的是这让代码难与管理,对阅读代码的人来说很难区分哪些变量是全局的。幸好,我们的匿名函数提供了一个简单的替代方案。将全局变量作为参数传入匿名函数,这比用隐含全局变量更清晰更快速。例子:
- (function ($, YAHOO) {
- // 使用全局的 jquery 比如$ 和 YAHOO
- }(jQuery, YAHOO));
模块导出
当你不仅仅想使用全局变量,还想声明一些(全局变量)的时候。我们可以很方便地用匿名函数的返回值来导出(全局变量)。 这么做就是一个完整的模块模式基本形态。例子:
- var MODULE = (function () {
- var my = {},
- privateVariable = 1;
- function privateMethod() {
- // …
- }
- my.moduleProperty = 1;
- my.moduleMethod = function () {
- // …
- };
- return my;
- }());
我们声明了一个全局变量”MODULE”, 有两个公有属性: 分别是一个方法MODULE.moduleMethod和一个变量MODULE.moduleProperty。除此之外,它用匿名函数的闭包保持自己的私有内部状态。同时根据上一个例子,我们还可以很方便的引用全局变量。
高级模式
上面的内容对大多数用户已经很足够了,但我们还可以基于此模式发展出更强大,易于扩展的结构。
增生
模块模式的一个限制是整个模块必须写在一个文件里。在大型编码项目里工作的人都知道代码分成多个文件的重要性。幸好,我们又一个很好的解决方案。首先,我们导入模块,然后我们添加属性,然后我们再把它导出。例子:
- var MODULE = (function (my) {
- my.anotherMethod = function () {
- //添加一些方法
- };
- return my;
- }(MODULE));
为确保一致性我们再次使用var关键字,尽管这不是必须的。代码运行后,我们的模块会获得一个新的公有方法MODULE.anotherMethod。这个增生的文件也保持自己的私密性,内部状态和对他的导入。
松散增生
我们的上一个例子要求我们的初始化模块必须先运行。而增生必须第二步发生。这不应该是必要的。JavaScript的好处之一就是可以异步的读取脚本文件。我们可以创建灵活的多块的模块,用Loose Augmentation,他们可以按任何顺序加载自己。每个文件应该有如下的结构:
- var MODULE = (function (my) {
- // 添加一些功能
- return my;
- }(MODULE || {}));
在这个模式下,var声明总是必须的注意如果模块还不存在,导入就会新建模块。这意味着你可以使用类似LABJavaScript这样的工具并行的读取所有你的模块文件,没有任何的阻塞。
紧密增生
虽然松散增生很牛叉,但是这对你的模块有一定的限制。最重要的是你不能安全的重载(override)你的模块属性.你也不能在初始化的时候就使用模块的属性。紧密增生包含一个加载顺序,但是允许重载(override).例子:
- var MODULE = (function (my) {
- var old_moduleMethod = my.moduleMethod;
- my.moduleMethod = function () {
- // 重载方法,可通过old_moduleMethod调用旧的方法
- };
- return my;
- }(MODULE));
这样我们重载(override)了MODULE.moduleMethod,但如果需要,仍可保持对原方法的引用。
- Cloning and Inheritance 克隆和继承
- var MODULE_TWO = (function (old) {
- var my = {},
- key;
- for (key in old) {
- if (old.hasOwnProperty(key)) {
- my[key] = old[key];
- }
- }
- var super_moduleMethod = old.moduleMethod;
- my.moduleMethod = function () {
- //在克隆里重载方法,通过super_moduleMethod接入父级(super)
- };
- return my;
- }(MODULE));
这个模式可能是最灵活的选择。他允许一些neat compositions。这会带来一些灵活性上的代价。
跨文件私有状态
将一个模块划分到多个文件的限制之一是每个文件保持它自己的私有状态,而且不能解接入其他文件的私有状态。这是可以解决的,下面的例子用松散增生模块在多个增生之间保持私有状态:
- var MODULE = (function (my) {
- var _private = my._private = my._private || {},
- _seal = my._seal = my._seal || function () {
- delete my._private;
- delete my._seal;
- delete my._unseal;
- },
- _unseal = my._unseal = my._unseal || function () {
- my._private = _private;
- my._seal = _seal;
- my._unseal = _unseal;
- };
- // 持久的接入 _private, _seal, 和 _unseal
- return my;
- }(MODULE || {}));
任何文件都可以对他们的局部变量_private设属性,并且设置对其他的文件也立即生效。一旦这个模块加载结束,应用会调用 MODULE._seal()"上锁",这会阻止外部接入内部的_private。如果这个模块需要再次增生,应用的生命周期内,任何文件都可以调用_unseal() ”开锁”,然后再加载新文件。加载后再次调用 _seal()”上锁”。
子模块
我们最后的高级模式是最简单的,有很多好例子来创建子模块,就像创建一个普通的模块:
- MODULE.sub = (function () {
- var my = {};
- // …
- return my;
- }());
尽管这很明显,但我认为还是值得加进来的,子模块具有一般模块所有的高级能力,包括增生和私有状态。
总结
大多数高级模式可以与其他的互相组合形成有用的模式。如果让我来设计一个复杂应用的架构,我会组合使用loose augmentation, private state, 和 sub-modules.
这里我并没有研究性能问题。但是我想顺便提一句:模块模式效率很好。代码量可以更少,使加载代码更快。使用 loose augmentation允许简单的非阻碍式并行加载,这更可以提升下载的速度。初始化时间可能比其他方法要慢,但是这是值得的。
最后,这里有个sub-module动态的把自己加载到父模块去(如果没有则创建)。为了简洁,这里没有包含private state。这段代码允许一个复杂的大型分层代码库并行的加载自己和它的子模块:
- var UTIL = (function (parent, $) {
- var my = parent.ajax = parent.ajax || {};
- my.get = function (url, params, callback) {
- // ok, so I’m cheating a bit
- return $.getJavaScriptON(url, params, callback);
- };
- // etc…
- return parent;
- }(UTIL || {}, jQuery));
原文链接:http://www.douban.com/group/topic/10456277/