The Module Pattern
Modules
模块是任何健壮的应用程序架构的一个完整部分,并且通常用来帮助保持一个项目代码单元既整洁又有条理。
在JS中,有如下几个关于实现模块的选项,他们包括
1.模块模式
2.对象字面量表示法
3.AMD 模块
4.CommonJs 模块
5.ECMAScript Harmony 模块
我们将在后面探索上述的后三种模式。
模块模式部分是基于对象字面量的,所以首先认识这个知识对我们来说是有意义的。
对象字面量
对象字面量是一个对象,通常用一组包含在{}大括号中的键值对描述。在对象字面量中的名称通常是后跟冒号的字符串或者标识符。
在对象的最后一组键值对之后,不应该有逗号,否则可能会导致错误。
var myObjectLiteral = {
variableKey: variableValue,
functionKey: function () {
// ...
}
};
对象字面量不需要使用关键词new 运算符创建,但是也不应该把它用于一段代码声明的开始,就像打开的花括号{通常被解释为一个块的开始。在对象之外,一个新的属性可以通过赋值的方式被添加进对象,例如 myModule.property = "SOME VALUE"
通过下面的例子我们可以看到一个通过复杂字面量定义的模块的例子。
var myModule = {
myProperty: "someValue",
//对象字面量可以包含属性和方法,例如我们可以定一个对象来表是这个模块的配置
myConfig: {
useCaching: true,
language: "en"
},
// 一个基本的方法
saySomething: function () {
console.log( "Where in the world is Paul Irish today?" );
},
// 一个基于现有配置进行输出的方法
reportMyConfig: function () {
console.log( "Caching is: " + ( this.myConfig.useCaching ? "enabled" : "disabled") );
},
// 重写现有配置的方法
updateMyConfig: function( newConfig ) {
if ( typeof newConfig === "object" ) {
this.myConfig = newConfig;
console.log( this.myConfig.language );
}
}
};
// =>: Where in the world is Paul Irish today?
myModule.saySomething();
// =>: Caching is: enabled
myModule.reportMyConfig();
// =>: fr
myModule.updateMyConfig({
language: "fr",
useCaching: false
});
// =>: Caching is: disabled
myModule.reportMyConfig();
利用对象字面量可以帮助你封装和组织你的代码。如果我们选择这种技术,我们可能同样会对模块模式感兴趣,它始终利用对象字面量,但仅仅是作为一个函数作用域的返回值。
The Module Pattern
模块模式最初被定义为在传统的软件工程中为类定义提供公有和私有封装的一种方式。
在JS中,模块模式被用来进一步模拟类的概念,通过在一个单例对象中包含公有的/私有的方法和变量,从全局作用域中屏蔽具体的细节部分。
这样做的结果是我们减少了我们的函数名同该页面上被定义的其他附加的函数冲突的可能性。
私有
模块模式利用闭包来封装私有的、状态或者组织。它提供一种封装公有和私有方法的混合方法,保护内部的内容不泄露到全局的作用域中并与其他的开发人员的接口相冲突。在这种模式中,只有公有的方法会被返回,将所有其他的内容都保留在了闭包内。
这给我们提供了一个简洁的解决方案,屏蔽内部复杂逻辑并仅仅暴露我们的应用其他部分希望使用的接口,该模式和立即被调用的函数表达式非常相似,除了返回一个对象而不是一个函数。
需要注意的是,js内部确实没有真正意义上的私有,因为它不像一些传统的语言,它没有访问修饰符。变量技术上不可以直接被声明为共有的或者私有的,所以我们使用函数作用域来模拟这个概念,在模块模式内部,被声明的变量或者方法,只能在模块内部使用,这归功于闭包。然而在返回的对象中定义被定义的变量或者方法可以被任何人使用。
History
从历史的角度看,模块模式起初是被一群包括Richard Cornford的人在2003年开发出来的。后来被道格拉斯克罗克福德(Douglas Crockford)在他的演讲中推广。另一件小事是,如果你曾经使用过Yahoo的YUI库,其中的一些功能呈现出这种十分熟悉的原因是模块模式对YUI创建他们的组件时有强烈的影响。
例子
让我们通过创建一个独立的模块来看看如何实现模块模式
var testModule = (function () {
var counter = 0;
return {
incrementCounter: function () {
return counter++;
},
resetCounter: function () {
console.log( "counter value prior to reset: " + counter );
counter = 0;
}
};
})();
// 用法
// 增加数量
testModule.incrementCounter();
// 测试结果
// =>: counter value prior to reset: 1
testModule.resetCounter();
这里,代码的其他部分不可以直接访问incrementCounter()
和resetCounter()
。这里的counter变量实际上是被我们从全局作用域屏蔽掉了,所以它看上去扮演的是私有变量——它的存在被限制在了模块的闭包之中,也因此我们能够在模块外直接访问的代码只有两个函数。我们的命名空间是有效的,因此在我们测试我们代码的环节,我们需要在任何调用的时候,加入模块名作为前缀。
当使用模块模式时,我们可能会发现我们定义一个简单的模板,对开始使用它是有用的。这有一个包含命名空间,公有/私有变量的例子:
var myNamespace = (function () {
var myPrivateVar, myPrivateMethod;
// 一个私有的计数器变量
myPrivateVar = 0;
// 一个私有的方法,可以打印任何输入的变量
myPrivateMethod = function( foo ) {
console.log( foo );
};
return {
// 一个公有的变量
myPublicVar: "foo",
// 一个利用私有变量的公有函数/方法
myPublicFunction: function( bar ) {
//增加我们的私有变量
myPrivateVar++;
//调用我们私有的方法
myPrivateMethod( bar );
}
};
})();
来看另一个例子,如下我们可以看到一个使用这种模式实现的购物篮。这个模块是完全独立的在,在全局变量中被叫做basketModule
。其中basket
数组在模块中保持私有状态,所以我们应用中的其他部分是不可以直接读取它的。它仅仅存在于模块的闭包之中,并且在模块外部能访问的仅仅是它暴露出的方法,例如addItem()
,getItem()
。
var basketModule = (function () {
// privates
var basket = [];
function doSomethingPrivate() {
//...
}
function doSomethingElsePrivate() {
//...
}
// Return an object exposed to the public
return {
// Add items to our basket
addItem: function( values ) {
basket.push(values);
},
// Get the count of items in the basket
getItemCount: function () {
return basket.length;
},
// Public alias to a private function
doSomething: doSomethingPrivate,
// Get the total value of items in the basket
getTotal: function () {
var q = this.getItemCount(),
p = 0;
while (q--) {
p += basket[q].price;
}
return p;
}
};
})();
你可能会注意到,在模块内部,我们返回了一个object,他们会被自动的赋值给basketModule,以便我们可以按照如下的方式与之互动。
// basketModule 模块返回了一个我们可以使用的公用API对象。
basketModule.addItem({
item: "bread",
price: 0.5
});
basketModule.addItem({
item: "butter",
price: 0.3
});
// => 2
console.log( basketModule.getItemCount() );
// => 0.8
console.log( basketModule.getTotal() );
// 然而下面的方式将不会工作
// => undefined
// 这是因为这个属性没有被这个模块作为公有的API暴露出来
console.log( basketModule.basket );
// 不工作,理由同上,只存在于闭包内,没有被作为公有的API暴露出来。
console.log( basket );
以上方法在命名空间(basketModule)内部都是有效的。
注意如何界定上面的basket模块中包含的功能,即我们之后立即调用并存储的返回值。这里有一系列的优点如下:
1.拥有仅供我们模块使用的公有和私有成员的自由,因为它们没有暴露在页面的其他部分(除了我们暴露的公有API),它们可以被看作是私有的。
2.函数被正常的声明和命名,当我们试图去发现哪个函数抛出异常时将容易在调试工具中看到函数调用栈。
3.T. J Crowder指出,在过去,它也使我们能够在不同的环境中返回不同的功能。在过去,我已经看到开发人员使用这个进行UA测试,为了针对IE浏览器在他们的模块中提供了一个代码路径,但在当下我们可以选择特性针对完成相似的目标。
Module Pattern Variations
Import mixins(导入混入?)
这种模式的变化,展示了如何将全局变量作为函数参数,传递给一个定义我们模块的匿名函数。这允许我们有效的将它导入到我们代码的作用域并且命名为我们希望的别名。
// 此处可以起别名
var myModule = (function ( jQ, _ ) {
function privateMethod1(){
jQ(".container").html("test");
}
function privateMethod2(){
console.log( _.min([10, 5, 100, 2, 1000]) );
}
return{
publicMethod: function(){
privateMethod1();
}
};
// 将jQuery 和 Underscore导入到匿名函数中
})( jQuery, _ );
myModule.publicMethod();
Exports(导出)
接下来的这个变化允许我们声明全局变量而不去使用它,有点像支持导入全局变量的概念,在接下来的例子中我们可以看到。
// Global module
var myModule = (function () {
//模块对象
var module = {},
privateVariable = "Hello World";
function privateMethod() {
// ...
}
module.publicProperty = "Foobar";
module.publicMethod = function () {
console.log( privateVariable );
};
return module;
})();
工具包和特定框架的模块模式实现
Dojo
Dojo提供了一些便利的方法处理对象,通过对象的方法调用dojo.setObject()
。用点分割的字符串,作为它的第一个参数,就像myObject.parent.child
中一个“child"作为"parent"的一个属性,而"parent"则定义在"myObj"中。使用 setObject()
允许我们去设置它的子对象,如果在给定的路径中对象不存在的话,创建中间的对象。
例如你想创建一个baseket.core
作为store
命名空间下的对象,这可以用下面的一般性方式实现。
var store = window.store || {};
if ( !store["basket"] ) {
store.basket = {};
}
if ( !store.basket["core"] ) {
store.basket.core = {};
}
store.basket.core = {
// ...rest of our logic
};
或者使用 Dojo 1.7(AMD兼容版)这个库并配合如下代码实现
require(["dojo/_base/customStore"], function( store ){
// 使用 dojo.setObject()
store.setObject( "basket.core", (function() {
var basket = [];
function privateMethod() {
console.log(basket);
}
return {
publicMethod: function(){
privateMethod();
}
};
})());
});
如果想了解更多的关于dojo.setObject
的信息,请查看官方文档
ExtJS
关于那些使用了Sencha的ExtJs的,下面的一个例子告诉你,如何利用框架正确的使用模块模式。
这里,我们看到了一个关于如何定义命名空间,然后利用包含公有和私有的API模块填充它的例子。除了一些语法不同外,它与VanillaJs中的模块模式的实现非常接近。
// 创建命名空间
Ext.namespace("myNameSpace");
// 在命名空间中创建应用
myNameSpace.app = function () {
// 不要在这里创建引用DOM或者DOM元素
// 私有变量
var btn1,
privVar1 = 11;
// 私有方法
var btn1Handler = function ( button, event ) {
console.log( "privVar1=" + privVar1 );
console.log( "this.btn1Text=" + this.btn1Text );
};
// 公有方法
return {
// 公有属性
btn1Text: "Button 1",
// 公有方法
init: function () {
if ( Ext.Ext2 ) {
btn1 = new Ext.Button({
renderTo: "btn1-ct",
text: this.btn1Text,
handler: btn1Handler
});
} else {
btn1 = new Ext.Button( "btn1-ct", {
text: this.btn1Text,
handler: btn1Handler
});
}
}
};
}();
YUI
相似的,当我们构建我们的应用程序的时候,也可以使用YUI3实现模块模式。
接下来的这个例子严重的依赖原来的YUI模块模式,由Eric Miraglia实现,但是,它与vanillaJs的版本存在着极大的不同。
Y.namespace( "store.basket" ) ;
Y.store.basket = (function () {
var myPrivateVar, myPrivateMethod;
// 私有变量:
myPrivateVar = "I can be accessed only within Y.store.basket.";
// 私有方法:
myPrivateMethod = function () {
Y.log( "I can be accessed only from within YAHOO.store.basket" );
}
return {
myPublicProperty: "I'm a public property.",
myPublicMethod: function () {
Y.log( "I'm a public method." );
// 在basket里面我们可以使用私有变量和方法
Y.log( myPrivateVar );
Y.log( myPrivateMethod() );
// 通过this访问该作用域(当前返回对象)里的方法
Y.log( this.myPublicProperty );
}
};
})();
jQuery
这里有一些方法,jQuery代码不指定插件也可以包裹在模块模式中。Ben Cherry之前建议过一种实现,将一些含有共同点的模块,通过包裹在函数中定义为一个模块。
在下面的例子中,一个library
方法被定义,它声明了一个库,并在它被创建的时候,自动的将它的inti方法绑定到了document.ready
方法中。
function library( module ) {
$( function() {
if ( module.init ) {
module.init();
}
});
return module;
}
var myLibrary = library(function () {
return {
init: function () {
// module implementation
}
};
}());
优点
我们已经看到了模块模式为何是有用的,但是为什么模块模式是一个好的选择?对于一个初学者来说,有着面向对象背景的一个开发者来说,相比真正的封装而言,这样会更整洁,至少从JS角度来看是这样。
其实,它支持私有数据,所以在模块模式中,我们的公有部分的代码可以接触到私有的部分,然而模块外的世界则不可接触到模块中的私有部分。
缺点
我们访问公有的成员和私有的成员方式不同,所以当我们想要改变成员的可访问性时,我们实际上需要修改每个用到模块成员的地方。
我们也无法在方法中访问之后被添加到对象的私有成员。也就是说,在许多情况下模块模式仍是非常有用的,当使用正确,当然有潜力干山我们的应用程序结构。
其他的缺点包括无法为私有成员创建单元测试,当错误需要热修复时,增加了额外的复杂度。修复私有的地方简直是不可能的。相反,一个人必须修改所有的与有bug的私有成员交互的公有成员,开发者也无法轻易的扩展私有成员,所以值得记住的是私有成员并不像最初呈现的那样灵活。
关于更多关于模块模式的内容,可以参考Ben Cherry的优秀的深入的文章。