<译自http://www.adequatelygood.com/2010/3/JavaScript-Module-Pattern-In-Depth>
<译者按:一个具有一定复杂度的应用程序必须是模块化的。Javascript语言本身具有一些不适合模块化的特性,比如过多依赖全局变量,没有名字空间,基于原型的继承等等,这些给Javascript的模块化设计带来一定难度。这篇文章通过对一些流行框架的设计模式进行解析,规纳出一些重要的模块设计模式。无论是对我们阅读流行的Javascript框架,还是在不使用Javascript库的情况下编写自己的模块乃至于框架,都将有所助益。>
模块设计模式是常见的Javascript设计模式。这些模式基本上比较容易理解,但仍有一些高级用法并没有引起广泛重视。在这篇文章里,我将讨论模块设计模式的基础,以及一些真正值得重视的高级话题,其中有一些见解我自认为是十分独到的。
基本模式
让我们先简单回顾一些基本的设计模式,这些设计模式三年前首次被Eric Miraglia (YUI开发人员)发表在他的博客中。如果你对这些模式比较熟悉,可以跳过直接去看高级设计模式。
匿名闭包
这是一切功能得以实现的基础结构,也是Javascript最好的特性。简单地创建一个匿名函数并且立即执行它。所有在函数中执行的代码生存在一个闭包(closure)之中,闭包在应用程序的全生命期提供了这些代码的私有性(privacy)和状态。
(function () {
// … all vars and functions are in this scope only
// still maintain access to all globals
}());
注意匿名函数之外的一对括号。这是由Javascript的语言特性所要求的。在Javascript中,由关键字function开头的语句总是被认为是函数声明,将其用一对括号括起来就生成了一个函数表达式。
导入全局变量
Javascript有一个称作隐式全局作用域的特性。当一个变量名被使用时,解析器遍历作用域链以查找关于这个变量的var声明语句。如果找不到,则这个变量就认为是全局变量。如果是在一个赋值语句中被使用,且该变量此前不存在,则一个新的全局变量被创建。这意味着很容易在闭包中使用/创建一个全局变量。不幸地是,这会导致难于维护的代码,因为对人来说,很难发现指定文件中哪些变量是全局变量。
幸运地是,匿名函数提供了一个容易的变通方法。通过将全局变量作为参数传入匿名函数,我们把它们导入到函数内部,由此代码既清晰,又快速(查找本地变量比全局变量要快)。这里有一个例子:
(function ($, YAHOO) {
// now have access to globals jQuery (as $) and YAHOO in this code
}(jQuery, YAHOO));
模块导出
(function ($, YAHOO) {
// now have access to globals jQuery (as $) and YAHOO in this code
}(jQuery, YAHOO));
有时候你不只是想使用全局变量,而且想声明一个全局变量。我们可以通过匿名函数的return value轻易地导出它们。 如此一来我们就完成了最基础的模块设计模式,完整示例如下:
var MODULE = (function () {
var my = {},
privateVariable = 1;
function privateMethod() {
// ...
}
my.moduleProperty = 1;
my.moduleMethod = function () {
// ...
};
return my;
}());
注意这里我们声明了一个名为MODULE的全局模块名字,并包含两个公共属性:一个名为MODULE.moduleMethod的方法和一个名为MODULE.moduleProperty的变量。此外,它通过闭包维护了一个私有的内部状态。同时,通过使用前面学到的模式,我们也可以轻松地导入全局变量。
高级模式
尽管上面的模式已堪对付大多数应用,但我们还可以在此基础上更进一步,创造一些更强大,更易扩展的结构。让我们从已定义的模块MODULE开始,逐一讨论。
扩张模式(Augmentation)
前述模式的缺陷之一是,整个模块必须定义在一个文件里。那些在较大代码量工程工作过的人都能理解将代码分散到多个文件的价值。幸运地是,我们有了一个扩张模块的很好的解决办法。首先,我们导入一个模块,然后增加新属性,然后再导出它们。请看例子:
var MODULE = (function (my) {
my.anotherMethod = function () {
// added method...
};
return my;
}(MODULE));
松耦合扩张 (Loose Augmentation)
var MODULE = (function (my) {
my.anotherMethod = function () {
// added method...
};
return my;
}(MODULE));
前面讲到的代码依赖于初始化模块必须首先创建,然后扩张才能进行。可是这并不总是成立。Javascript提升性能的方法之一就是异步加载脚本。通过松耦合扩张,我们就可以将多块的脚本以任意次序载入。每个文件都应该有着如下的结构:
var MODULE = (function (my) {
// add capabilities...
return my;
}(MODULE || {}));
在此模式中,var声明始终是必须的。注意导入部分总是会创建模块,如果事前不存在的话。这意味着你可以使用类似 LABjs的工具并发地载入你所有的模块文件,而不是阻塞式地依次载入。
紧耦合扩张 (Tight Augmentation)
松耦合扩张很不错,但它也给你的模块带来一些限制。最重要地是,你无法安全地重载模块的属性,同时,你也不能在初始化期间使用定义在其它文件中的模块属性(但你可以在初始化完成后这样做)。紧耦合扩张依然强调加载次序,但允许重载。下面是一个简单的例子:
var MODULE = (function (my) {
var old_moduleMethod = my.moduleMethod;
my.moduleMethod = function () {
// method override, has access to old through old_moduleMethod...
};
return my;
}(MODULE));
此处我们重载<译注:原文为overridden,实为改写)了MODULE.moduleMethod,但维护着一个对原方法的引用,以备不时之需。
克隆和继承
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 () {
// override method on the clone, access to super through super_moduleMethod
};
return my;
}(MODULE));
本模式或许是最缺乏灵活性的一个模式。它的确使用一些简洁的组合成为可能,但却以灵活性为代价。正如所示的那样,对象和函数属性并不会被复制,它们会以一个对象,两个引用的方式存在,改变其中的一个(<译注:指引用>)会使得另一个也被改变。对对象进行递归克隆可以修正这个问题,但可能无助于函数属性,除了使用eval的函数外。无论如何,为完备性起见,我也将此模式包含进来。
跨文件的私有状态
将模块切分到多个文件的一个严重问题是,每个文件维护着自己的私有状态,而不能访问其它文件里的私有状态。这个问题是可以修复的。下例说明了松耦合扩张模式下,模块如何在各文件间共享但又保持状态的私有性。
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;
};
// permanent access to _private, _seal, and _unseal
return my;
}(MODULE || {}));
每个文件只须将私有属性置于变量_private之中,便可为其它文件访问。一旦该模块加载完全,应用程序应该调用MODULE._seal(),以防止外部对 _private的访问。如果模块需要再次扩张,将来在应用程序的生命期内,只需要加载新文件之前,在任何文件中调用一次内部方法_unseal(),并在加载之后调用一次_seal()。
本模式是我今天在工作时突然冒出的灵感,我以前没有在任何地方见过这种模式。我认为它将是一个很有用的模式,值得记录下来。
子模块
最后要讲的一个高级设计模式实际上相当简单。有许多情况下要创建子模块。创建子模块就跟创建普通模块一样:
MODULE.sub = (function () {
var my = {};
// ...
return my;
}());
结论
MODULE.sub = (function () {
var my = {};
// ...
return my;
}());
多数高级设计模式可以组合在一起以创建更加有用的模式。如果我必须为设计复杂应用程序设计提供一条路径的话,我会将松耦合扩张,私有状态和子模块结合在一起。
这里我没有提到性能问题,但我愿意简短地说一句:模块设计模式有利于提升性能。它使得程序大为精简,这使得下载变得更为迅速。使用松耦合扩张模式允许异步下载,这也增加了下载速度。初始化时间可能会变长一点,但值得为之付出代价。运行时性能应该没有损失,只要全局变量被正确地导入,反而极有可能会加速子模块运行--因为本地变量缩短了引用链。
作为结束语,下面给出一个能动态将自身载入父模块的例子。为简明起见,我移除了私有状态,要将其包含进来也是很容易的事。这段代码模板允许复杂的多层代码并行加载。
var UTIL = (function (parent, $) {
var my = parent.ajax = parent.ajax || {};
my.get = function (url, params, callback) {
// ok, so I'm cheating a bit :)
return $.getJSON(url, params, callback);
};
// etc...
return parent;
}(UTIL || {}, jQuery));