javascript 模块化编程
javascript 模块化编程
The module pattern is a common JavaScript coding pattern. It’s generally well understood, but there are a number of advanced uses that have not gotten a lot of attention. In this article, I’ll review the basics and cover some truly remarkable advanced topics, including one which I think is original.
The Basics
We’ll start out with a simple overview of the module pattern, which has been well-known since Eric Miraglia (of YUI) first blogged about it three years ago. If you’re already familiar with the module pattern, feel free to skip ahead to “Advanced Patterns”.
Anonymous Closures
This is the fundamental construct that makes it all possible, and really is the single best feature of JavaScript. We’ll simply create an anonymous function, and execute it immediately. All of the code that runs inside the function lives in a closure, which provides privacy and state throughout the lifetime of our application.
(function () {
// ... all vars and functions are in this scope only
// still maintains access to all globals
}());
Notice the ()
around the anonymous function. This is required by the language, since statements that begin with the token function
are always considered to be function declarations. Including ()
creates a function expression instead.
Global Import
JavaScript has a feature known as implied globals. Whenever a name is used, the interpreter walks the scope chain backwards looking for a var
statement for that name. If none is found, that variable is assumed to be global. If it’s used in an assignment, the global is created if it doesn’t already exist. This means that using or creating global variables in an anonymous closure is easy. Unfortunately, this leads to hard-to-manage code, as it’s not obvious (to humans) which variables are global in a given file.
Luckily, our anonymous function provides an easy alternative. By passing globals as parameters to our anonymous function, we import them into our code, which is both clearer and faster than implied globals. Here’s an example:
(function ($, YAHOO) {
// now have access to globals jQuery (as $) and YAHOO in this code
}(jQuery, YAHOO));
Module Export
Sometimes you don’t just want to use globals, but you want to declare them. We can easily do this by exporting them, using the anonymous function’s return value. Doing so will complete the basic module pattern, so here’s a complete example:
var MODULE = (function () {
var my = {},
privateVariable = 1;
function privateMethod() {
// ...
}
my.moduleProperty = 1;
my.moduleMethod = function () {
// ...
};
return my;
}());
Notice that we’ve declared a global module named MODULE
, with two public properties: a method named MODULE.moduleMethod
and a variable named MODULE.moduleProperty
. In addition, it maintains private internal stateusing the closure of the anonymous function. Also, we can easily import needed globals, using the pattern we learned above.
Advanced Patterns
While the above is enough for many uses, we can take this pattern farther and create some very powerful, extensible constructs. Lets work through them one-by-one, continuing with our module named MODULE
.
Augmentation
One limitation of the module pattern so far is that the entire module must be in one file. Anyone who has worked in a large code-base understands the value of splitting among multiple files. Luckily, we have a nice solution to augment modules. First, we import the module, then we add properties, then we export it. Here’s an example, augmenting our MODULE
from above:
var MODULE = (function (my) {
my.anotherMethod = function () {
// added method...
};
return my;
}(MODULE));
We use the var
keyword again for consistency, even though it’s not necessary. After this code has run, our module will have gained a new public method named MODULE.anotherMethod
. This augmentation file will also maintain its own private internal state and imports.
Loose Augmentation
While our example above requires our initial module creation to be first, and the augmentation to happen second, that isn’t always necessary. One of the best things a JavaScript application can do for performance is to load scripts asynchronously. We can create flexible multi-part modules that can load themselves in any order with loose augmentation. Each file should have the following structure:
var MODULE = (function (my) {
// add capabilities...
return my;
}(MODULE || {}));
In this pattern, the var
statement is always necessary. Note that the import will create the module if it does not already exist. This means you can use a tool like LABjs and load all of your module files in parallel, without needing to block.
Tight Augmentation
While loose augmentation is great, it does place some limitations on your module. Most importantly, you cannot override module properties safely. You also cannot use module properties from other files during initialization (but you can at run-time after intialization). Tight augmentation implies a set loading order, but allows overrides. Here is a simple example (augmenting our original MODULE
):
var MODULE = (function (my) {
var old_moduleMethod = my.moduleMethod;
my.moduleMethod = function () {
// method override, has access to old through old_moduleMethod...
};
return my;
}(MODULE));
Here we’ve overridden MODULE.moduleMethod
, but maintain a reference to the original method, if needed.
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 () {
// override method on the clone, access to super through super_moduleMethod
};
return my;
}(MODULE));
This pattern is perhaps the least flexible option. It does allow some neat compositions, but that comes at the expense of flexibility. As I’ve written it, properties which are objects or functions will not be duplicated, they will exist as one object with two references. Changing one will change the other. This could be fixed for objects with a recursive cloning process, but probably cannot be fixed for functions, except perhaps with eval
. Nevertheless, I’ve included it for completeness.
Cross-File Private State
One severe limitation of splitting a module across multiple files is that each file maintains its own private state, and does not get access to the private state of the other files. This can be fixed. Here is an example of a loosely augmented module that will maintain private state across all augmentations:
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 || {}));
Any file can set properties on their local variable _private
, and it will be immediately available to the others. Once this module has loaded completely, the application should call MODULE._seal()
, which will prevent external access to the internal _private
. If this module were to be augmented again, further in the application’s lifetime, one of the internal methods, in any file, can call _unseal()
before loading the new file, and call _seal()
again after it has been executed. This pattern occurred to me today while I was at work, I have not seen this elsewhere. I think this is a very useful pattern, and would have been worth writing about all on its own.
Sub-modules
Our final advanced pattern is actually the simplest. There are many good cases for creating sub-modules. It is just like creating regular modules:
MODULE.sub = (function () {
var my = {};
// ...
return my;
}());
While this may have been obvious, I thought it worth including. Sub-modules have all the advanced capabilities of normal modules, including augmentation and private state.
Conclusions
Most of the advanced patterns can be combined with each other to create more useful patterns. If I had to advocate a route to take in designing a complex application, I’d combine loose augmentation, private state, and sub-modules.
I haven’t touched on performance here at all, but I’d like to put in one quick note: The module pattern is good for performance. It minifies really well, which makes downloading the code faster. Using loose augmentation allows easy non-blocking parallel downloads, which also speeds up download speeds. Initialization time is probably a bit slower than other methods, but worth the trade-off. Run-time performance should suffer no penalties so long as globals are imported correctly, and will probably gain speed in sub-modules by shortening the reference chain with local variables.
To close, here’s an example of a sub-module that loads itself dynamically to its parent (creating it if it does not exist). I’ve left out private state for brevity, but including it would be simple. This code pattern allows an entire complex heirarchical code-base to be loaded completely in parallel with itself, sub-modules and all.
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));
I hope this has been useful, and please leave a comment to share your thoughts. Now, go forth and write better, more modular JavaScript!
中文解释:
基础知识
首先我们开始简单概述模型模式。三年前Eric Miraglia(YUI)的博文使模型模式众所周知。如果你已经很熟悉模型模式,可以直接阅读“高级模式”。
匿名闭包
这是一切成为可能的基础,也是JavaScript最好的特性。我们将简单的创建匿名函数,并立即执行。所有函数内部代码都在闭包(closure)内。它提供了整个应用生命周期的私有和状态。
(function () {
// ... all vars and functions are in this scope only
// still maintains access to all globals
}());
注意匿名函数周围的()。这是语言的要求。关键字function一般认为是函数声明,包括()就是函数表达式。
引入全局
JavaScript有个特性,称为隐性全局。使用变量名称时,解释器会从作用域向后寻找变量声明。如果没找到,变量会被假定入全局(以后可以全局调用)。如果会被分配使用,在还不存在时全局创建它。这意味着在匿名函数里使用全局变量很简单。不幸的是,这会导致代码难以管理,文件中不容易区分(对人而言)哪个变量是全局的。
幸好,匿名函数还有一个不错的选择。全局变量作为参数传递给匿名函数。将它们引入我们的代码中,既更清晰,又比使用隐性全局更快。下面是一个例子:
(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。而且,匿名函数的闭包还维持了私有内部状态。同时学会之上的内容,我们就很容易引入需要的全局变量,和输出到全局变量。
高级模式
对许多用户而言以上的还不足,我们可以采用以下的模式创造强大的,可扩展的结构。让我们使用MODULE模块,一个一个继续。
扩充
模块模式的一个限制是整个模块必须在一个文件里。任何人都了解长代码分割到不同文件的必要。还好,我们有很好的办法扩充模块。(在扩充文件)首先我们引入模块(从全局),给他添加属性,再输出他。下面是一个例子扩充模块:
var MODULE = (function (my) {
my.anotherMethod = function () {
// 此前的MODULE返回my对象作为全局输出,因此这个匿名函数的参数MODULE就是上面MODULE匿名函数里的my
};
return my;
}(MODULE));
我们再次使用var关键字以保持一致性,虽然其实没必要。代码执行后,模块获得一个新公开方法MODULE.anotherMethod。扩充文件没有影响模块的私有内部状态
松耦合扩充
上面的例子需要我们首先创建模块,然后扩充它,这并不总是必要的。提升JavaScript应用性能最好的操作就是异步加载脚本。因而我们可以创建灵活多部分的模块,可以将他们无顺序加载,以松耦合扩充。每个文件应有如下的结构:
var MODULE = (function (my) {
// add capabilities...
return my;
}(MODULE || {}));
这个模式里,var语句是必须的,以标记引入时不存在会创建。这意味着你可以像LABjs一样同时加载所有模块文件而不被阻塞。
紧耦合扩充
虽然松耦合很不错,但模块上也有些限制。最重要的,你不能安全的覆写模块属性(因为没有加载顺序)。初始化时也无法使用其他文件定义的模块属性(但你可以在初始化后运行)。紧耦合扩充意味着一组加载顺序,但是允许覆写。下面是一个例子(扩充最初定义的MODULE):
var MODULE = (function (my) {
var old_moduleMethod = my.moduleMethod;
my.moduleMethod = function () {
// method override, has access to old through old_moduleMethod...
};
return my;
}(MODULE));
我们覆写的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;
},//模块加载后,调用以移除对_private的访问权限
_unseal = my._unseal = my._unseal || function () {
my._private = _private;
my._seal = _seal;
my._unseal = _unseal;
};//模块加载前,开启对_private的访问,以实现扩充部分对私有内容的操作
// permanent access to _private, _seal, and _unseal
return my;
}(MODULE || {}));
任何文件都可以在本地的变量_private中设置属性,他会对别的扩充立即生效(即初始化时所有扩充的私有状态都保存在_private变量,并被my._private输出)。模块完全加载了,应用调用MODULE._seal()方法阻止对私有属性的读取(干掉my._private输出)。如果此后模块又需要扩充,带有一个私有方法。加载扩充文件前调用MODULE._unseal()方法(恢复my._private,外部恢复操作权限)。加载后调用再seal()。
子模块
最后的高级模式实际上最简单。有很多好方法创建子模块。和创建父模块是一样的:
MODULE.sub = (function () {
var my = {};
// 就是多一级命名空间
return my;
}());
虽然很简单,但我还是提一下。子模块有所有正常模块的功能,包括扩充和私有状态。