代码改变世界

【翻译】Javascript “组件模式” 深入研究

2012-02-20 15:53  刺客之家  阅读(2912)  评论(7编辑  收藏  举报

一、前言

这段时间学习js,又看到一篇讲javascript设计模式的好文章,尝试翻译出来,如果有什么疏漏和错误,烦请各位不吝指出,谢谢~

原文地址:http://www.adequatelygood.com/2010/3/JavaScript-Module-Pattern-In-Depth

 

二、译文

“组件模式”是一种很常用的Javascript编码模式。虽然已经被广泛的应用,但是还有一些高级的用途没有被关注。在这边文章里,我将针对几个比较特别的话题进行简单的阐述,其中的一些我认为应该是第一次被提及(作者原创)。

基础

我们从一个简单的组件模式开始说起,这个模式现在已经非常流行,最初是3年前由雅虎YUI的Eric Miraglia在其博客中提出的。如果你已经对这个模式很熟悉了,那么可以直接跳到“高级模式”一节。

匿名闭包(Anonymous Closures)

这是使得模式可以正常工作的语言基础,也实在是javascript最为实用的特性之一。我们可以简单的创建一个匿名函数,并且立刻执行它。所有运行在该函数里的代码叫做一个闭包,它提供了在整个应用程序生命周期中都有效的数据隐私控制以及状态保存功能。

(function () {      
// ... all vars and functions are in this scope only        
// still maintains access to all globals
}());

 

请注意()包围了匿名函数,这是语法的需要,因为以function开头的语句会被编译器认为的函数的定义,而加上()就变为了创建了一个函数表达式。

全局导入(Global Import)

JavaScript 有一个大家熟知的特性叫做implied globals,就是当使用一个变量的时候,解释器会沿着作用域链一直向上查找这个变量的定义,如果没找到,这个变量就会自动成为一个全局(global)变量。这意味着在一个匿名闭包里去创建一个全局变量变得非常简单。

但是,这产生了很难管理的代码问题,因为我们无法明显的知道一个给定的文件里究竟哪些变量是全局的

译者注:如果允许这么隐晦的写法来创建全局变量,则代码中使用的变量,很难知道是全局的还是局部的。)

幸运的是,我们的匿名函数提供了一个很简单的修改来解决这个问题。我们传入一个全局对象作为匿名函数的参数,我们利用这个参数导入我们的变量到全局对象中,这样做比隐式的声明全局变量要干净和快速的多。下面是一个例子

 

(function ($, YAHOO) {        
// now have access to globals jQuery (as $) and YAHOO in this code
}(jQuery, YAHOO));
 

模块导出(Module Export)

有时候我们不只是想定义一个模块,还想直接使用它,我们可以使用匿名函数的返回值来实现。如果这么做,就是实现了一个基本的“组件模式”,看下面的例子:

var MODULE = (function () {        
var my = {},privateVariable = 1;               
 function privateMethod() {                
// ...        
}
my.moduleProperty = 1;       
 my.moduleMethod = function () {                
// ...        
};                
return my;
}());

 

要注意的是我们定义了一个全局组件名字叫“MODULE”,他有两个属性:一个方法MODULE.moduleMethod以及一个属性MODULE.moduleProperty。并且,它通过匿名函数所在的闭包维护了一组私有的内部状态

译者注:privateVariable变量和privateMethod方法都是私有的,外部无法访问)。

这样的话,我们就可以用上面的模式简单的导入需要的全局变量。


高级模式(Advanced Patterns)

上面的模式已经可以解决应用中的许多问题,但是我们还可以在这个基础上扩展出很多强大的,易扩展的结构。让我们一个个看一下,继续用我们的组件“MODULE”做例子

增益模式(Augmentation)

使用上面的模式的缺点和限制之一,就是整个模块必须在一个文件里。任何在大型项目中工作的人都会意识到能将代码拆分成多个文件带来的价值。幸运的是,我们有一个不错的解决方案叫做“增益组件”。首先,我们导入组件,然后添加属性,然后再将其导出。下面是一个例子,为我们的MODULE组件添加增益特性:

var MODULE = (function (my) {        
my.anotherMethod = function () {                
// added method...        
};        
return my;
}(MODULE));

 

我们使用var关键字来保持一致性(尽管这不是必须的)。这段代码执行过后,我们的组件就增加了一个公开的方法叫做MODULE.anotherMethod。这个增加的文件也会同时保持它自己的私有状态。

译者注:通过增益模式,一个雷被切分成独立的文件,每个文件关注它内部需要的方法和属性,并且当所有文件合并执行后,可以成为一个完整的组件)

松耦合增益(Loose Augmentation)

我们上面的例子有一个缺点,就是需要MODULE模块先被创建,然后再执行子文件中的增益。这是一种紧耦合的做法。javascript性能优化中通常的做法是脚本的异步加载,这样我们就可以创建由多个文件组成的易扩展的组件,并且他们可以以任何顺序来加载,这叫做松耦合增益模式:

var MODULE = (function (my) {        
// add capabilities...               
 return my;
}(MODULE || {}));

 

在这个模式中,var关键字是必须的。注意我们的import点(参数)会自动判断组件是否存在,否则就创建一个空组件。这意味着你可以使用一些异步加载工具比如LABjs来并行加载你所有的js组件,而不需要block页面。

紧耦合增益(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));

 

这里我们重写了 MODULE.moduleMethod,同时也保存了旧的 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));

 

这种模式也许是最没有扩展性的选项了。它虽然看上去让代码更简洁,但是带来的是扩展性的损失。我们上面的代码中,所有的属性和方法都不是副本,他们以“一个对象2个引用”的方式存在。修改其中一个都会影响到另外一个。

跨文件私有状态(Cross-File Private State)

将组件分割到多个文件中的另外一个限制,就是每个文件只能访问他自己的私有状态,而没有权限访问其他文件的私有数据。这个问题可以解决,下面是一个“松耦合增益模式”的跨文件访问私有状态的版本:

 

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()再次将类对外部屏蔽。

译者注:注意_unseal方法会在对象里添加1个公开的属性和2个公开方法,其他文件就可以在my对象中对private属性进行扩展,然后利用_seal()方法删除公开的属性,保持对象的密封性)

这个模式是今天我在工作的时候想到的,我没有在其他地方见过这种用法,我认为这是一种十分有用的模式。

子模块(Sub-modules)

我们最后的一个高级模式实际上是最简单的,创建子模块有很多的好处,创建子模块就和创建普通模块一样:

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

本博客文章若非标记转载,均为原创,转载请注明出处~