深度剖析JavaScript模块化模式

作者 ben cherry ,译者 魏楷聪 发布于 2015年01月20日


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.

JavaScript的模块化模式是一种常见的编码模式。人们对它普遍有所了解,但一些高级的用法并没有得到很大的关注。在这篇文章中,将会回顾基础的以及涵盖一些真正杰出的高级主题,而其中有一个我认为是我自己独创的。

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”.

我们将开始对模块化模式做一个简单的概述,这个模式在三年前Eric Miraglia写了一个关于它的博客而被人们所了解。如果你已经熟悉模块化模式,随时跳到“高级模式”吧。

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.

这个基本结构让一切成为可能,并且它确实是JavaScript中一项最好的特性。我们将简单地创建一个匿名函数,并立即执行它。所有在这个函数里执行的代码都位于闭包里,闭包提供的私有化和状态,贯穿于我们的应用的生命周期。

(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.

请注意:包围匿名函数的()是JavaScript语言所要求的,因为以这个标记函数开头的语句总被认为是函数声明,包括用()创建一个函数表达式。

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.

JavaScript有一项特性叫隐式全局变量。当使用一个变量名时,解释器会反向沿着作用域链寻找这个变量名的var声明。如果什么都没找到,则假定变量是全局变量。如果一个变量并未声明而直接为其赋值则会被创建为全局变量。这意味着在匿名闭包中使用或者创建全局变量是容易的。但不幸的是,由于在给定的文件里全局变量是不明显的,导致难以管理代码。而幸运的是,匿名函数提供了一种简单的替代方法。通过将全局变量作为参数传递给匿名函数,从而进入到匿名函数里供调用,这样比隐式全局变量更清晰、更快捷。

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 state using the closure of the anonymous function. Also, we can easily import needed globals, using the pattern we learned above.

请注意:我们已经声明了一个叫“MODULE”的全局模块,带有两个公共属性:一个叫“moduleMethod”的方法和一个叫“moduleProperty”的变量。此外,它通过匿名闭包维护了私有的内部状态。同时,我们可以通过上面学习过的模式轻松地调用所需的全局变量。

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.

虽然前面所提到的已经够用了,但我们可以更进一步采取高级模式来创建一些非常强大的,可扩展的结构。让我们继续“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.

尽管是非必要的,但为了确保一致性,我们再次使用var这个关键字。当这段代码运行完,模块将会获得一个名叫“anotherMethod”的新的公有的方法。这个增强了的文件也会维护它的私有内部状态和传递给它的参数。

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:

在前面的例子中,首先我们创建模块,然后扩展,但那并不是必须的。提高JavaScript应用的性能最好的方式之一是异步加载脚本。我们可以创建灵活的多模块以便以任意的顺序通过降低耦合来加载。每个文件都应该具备以下结构:

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.

在这个模式中,var语句总是必须的。注意,如果模块不存在,则导入将会创建模块。这意味着你可以使用工具比如LABjs和并行加载所有的模块文件,而无需块。

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.

我们重写了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 () {

// 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.

这种模式也许是最灵活的选项。它允许一些整洁的成分,但是以牺牲灵活性为代价的。正如我编写的那样,作为对象或函数的属性将不会被复制,他们作为一个对象有两个引用而存在。改变其中一个将会相应地改变另一个。这是对递归对象复制处理的固定用法,但对于函数就可能不是固定的了,除非eval。不过,我把它写上是为了完整性。

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.

任何文件都可以为它们的局部变量_private设置属性,然后它将会立即生效。当这个模块完全加载完,这个应用程序需应该调用MODULE._seal()防止外部访问内部变量_private。如果这个模块再继续增强,进一步在应用程序的生命周期里,在任何文件里有一个内部方法,在加载新的文件之前可以调用_unseal(),然后在执行完后再调用_seal()。今天我在工作中看到这个模块,我从未在其它地方见过这种模式。我认为这是一种非常有用的模式,并且一直都值得写下来。

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 hierarchical 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));


查看英文原文:JavaScript Module Pattern In-Depth 

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容