javaScript设计模式-模块模式

Module (模块)模式

模块是任何强大应用程序架构中不可缺少的一部分,它通常能够帮助我们清晰地分离和组织项目中的代码单元。
在 Javascript 中,有几种用于实现模块的方法,包括:

  1. 对象字面量表示法

    对象字面量是对象定义的一种简要形式,目的在于简化创建包含大量属性的对象的过程。

  2. Module 模式

    Module 模式在某种程度上是基于对象字面量

  3. AMD 模块
  4. CommonJS 模块
  5. ECMAScript Harmony 模块
对象字面量

在对象字面量表示法中,一个对象被描述为一组包含在大括号 {} 中、以逗号分隔的 name/value 对。对象内的名称可以是字符串或标识符,后面跟着一个冒号。

1
2
3
4
5
6
var myObectLiteral = {
variableKey: variableValue,
functionKey: function() {
// ...
}
};

下面是一个更完整的实例,使用对象字面量表示法定义的模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
var myModule = {
myProperty: "someValue",
// 对象字面量可以包含属性和方法
// 例如,可以声明模块的配置对象
myConfig: {
useCaching: true,
language: "en"
},

// 基本方法
myMethod: function() {
console.log("where in the world is Paul Irish today?");
},

// 根据当前配置输出信息
myMethod2: function() {
console.log(
"Caching is:" + this.myConfig.useCaching ? "enabled" : "disabled"
);
},
// 重写当前的配置
myMethod3: function(newConfig) {
if (typeof newConfig === "object") {
this.myConfig = newConfig;
console.log(this.myConfig.language);
}
}
};

// 输出: where in the world is Paul Irish today?
MyModule.myMethod();

// 输出: Caching is:enabled
myMothed.myMethod2();

// 输出: fr
myMothed.myMethod3({
language: "fr",
useCaching: false
});
Module(模块)模式

Module 模式最初被定义为一种在传统软件工程中为类提供私有和公有封装的方法。

在 javaScript 中,Module 模式用于进一步模拟类的概念,通过这种方式,能够使一个单独的对象拥有公有/私有方法和变量,从而屏蔽来自全局作用域的特殊部分。

私有

Module 模式使用闭包封装“私有”状态和组织。它提供了一种包装混合公有/私有方法和变量的方式,防止其泄漏至全局作用域,并与别的开发人员的接口发生冲突。通过该模式,只需返回一个公有的 API, 而其他的一切都维持在私有的闭包里。

该模式除了返回一个对象而不是一个函数之外,非常类似于一个立即调用的函数表达式。

示例

这里通过创建一个自包含的模块来实现 Module 模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var testModule = (function(){
var counter = 0;
return {
incrementCounter: function(){
return ++counter;
},
restCounter: function() {
console.log("counter value prior to reset: " + counter);
counter = 0;
}
},
})();

// 使用
console.log(testModule.incrementCounter()) // 1
console.log(testModule.incrementCounter()) // 2
testModule.restCounter(); // counter value prior to reset: 2
console.log(testModule.incrementCounter()) // 1
// 于全局作用域隔离,被局限于模块的闭包内。
console.log(testModule.counter) // undefined

使用Module模式时,可能会觉得它可以用来定义一个简单的模板来入门使用。下面是一个包含命名空间、公有和私有变量的Module模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var myNamespace = (function(){
// 私有计数器变量
var myPrivateVar = 0;

// 记录所有参数的私有函数
var myPrivateMethod = function(foo) {
console.log(foo); // aaaa
};

return {

// 公有变量
myPublicVar: "foo",

//调用私有变量和方法的公有函数
myPublicFunction: function (bar) {
// 增加私有计数器值
myPrivateVar++;

//传入bar调用私有方法
myPrivateMethod(bar);
return myPrivateVar;
}
};


})()



console.log(myNamespace.myPublicVar) // foo

console.log( myNamespace.myPublicFunction('aaaa')); // 1

使用这种模式实现的购物车。模块本身是完全自包含在一个被称为baskeModule的全局变量中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
var baskeModule = (function(){
// 私有
var basket = [];

function doSomethingPrivate() {
// ...
};

function doSomethingElsePrivate(){
// ...
};

// 返回一个暴露出来的公有对象
return {
// 添加 item 到购物车
addItem: function(values) {
basket.push(values);
},

// 获取购物车里的item数
getItemCount: function() {
return basket.length;
},

// 私有函数的公有形式别名
doSomething: doSomethingPrivate,

// 获取购物车里所有的item的价格总和
getTotal: function(){
var itemCount = this.getItemCount(),
total = 0;

while(itemCount--){
total += basket[itemCount].price;
}
return total;
}
}

})();

baskeModule.addItem({
item: "鞋子",
price: 222,
});
console.log(baskeModule.getItemCount()); // 1
console.log(baskeModule.getTotal()); // 222

请注意上面的basket模块中的作用域函数是如何包裹在所有函数的周围,然后调用并立即存储返回值。这有很多优点,包括;

  1. 只有我们的模块才能享有拥有私有函数的自由。因为它们不会暴露于页面的其余部分(只会暴露我们输出的API),我们认为它们是真正的私有。
  2. 鉴于函数往往已声明并命名,在视图找到有哪些函数抛出异常时,这将使得在调试器中显示调用堆栈变得更容易。
  3. 它还可以让我们返回不同的函数。开发人员可以使用它来执行UA测试,从而针对IE在它们的模块内提供一个代码路径,但我们现在可以很容易地选择特征检测来实现类似的目的。

Module 模式变化

模式的这种变化演示了全局变量(如:jQuery,Underscore)如何作为参数传递给模块化的匿名函数。这允许我们引入它们,并按照我们所希望的为它们取个本地别名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 全局模块
var myModule = (function(jQ, _) {
function privateMethod1() {
jQ("#container").html("test");
}

function privateMethod2() {
console.log(_.min([10, 5, 20, 30, 1000])); // 5
}

return {
publicMethod: function(){
privateMethod1();
privateMethod2();
}
};
})(jQuery, _);

// 使用
myModule.publicMethod()
引出

下一个变化允许我们声明全局变量,而不需实现它们,并可以同样地支持上一个示例中的全局引入的概念。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var myModule = (function(){

// 模块对象 声明全局变量
var module = {},
privateVariable = "Hello World";

function privateMethod() {
// ...
console.log(module)
}

module.publicProperty = "Foobar";

module.publicMethod = function() {
console.log(privateVariable);
}
return module;

})();
工具包和特定框架的Module模式实现

Dojo, 提供了一种和对象一起用的便利方法 dojo.setObject().其第一个参数是用点号分割的字符串,如 myObj.parent.child,它在parent对象中引用一个称为child的属性,parent对象是在myObj内部定义。我们可以使用 setObject() 设置子级的值(比如属性等),如果中间对象不存在的话,也可以通过点号分割将中间的字符串作为中间对象进行创建。

例如,如果要将basket.score声明为store名称空间的对象,可以采用传统的方法来实现。

1
2
3
4
5
6
7
8
9
10
11
12
var store = window || {};
if(!store['basket']) {
store.basket = {};
}

if(!store.basket["score"]) {
store.basket.score = {};
}

store.basket.core = {
//...剩余的逻辑
};

// 使用Dojo(AMD兼容的版本)和上述方法,如下所示:
dojo.setObject的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
require(["dojo/_base/customStore"], function(store) {
//使用 dojo.setObject()
store.setObject("basket.core", function(){
var basket = [];

function privateMethod() {
console.log(basket);
}
return {
publicMethod: function(){
privateMethod();
}
};
}());
});
ExtJS

Sencha ExtJS
演示了EXTJS框架如何正确使用Module模式
这里是一个示例;如何定义一个名称空间,然后填充一个包含私有和公有API的模块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

// 创建命名空间
Ext.namespace("myNameSpace");

// 创建应用程序
myNameSpace.app = function() {
// 这里不要访问 DOM, 因为元素还不存在
// 私有变量
var btn1,
privVarl = 11;

// 私有函数
var ben1Handler = function (button, event) {
console.log("privVarl", privVarl);
console.log("this.btnText=" + this.btnText);
};

// 公有对象
return {
// 公有属性,例如要转化的字符
btn1Text: "Button 1",

// 公有方法
init: function() {
if(Ext.Ext2) {
btn1 = new Ext.Button({
renderTo: 'btn1-ct',
text: this.btn1Text,
handler: ben1Handler
});
}else {
btn1 = new Ext.Button("btn1-ct",{
text: this.btn1Text,
handler: btn1Handler
});
}
}
};
}();
YUI

在使用YUI构建应用程序时,我们也可以实现Module模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Y.nameSpace("store.basket") = (function() {
var myPrivateVar, myPrivateMethod;

// 私有变量
myPrivateVar = 'I can be accessed only within YAHOO.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());

// myPublicMethod的原始作用域是store,所以可以使用this来访问公有成员
Y.log(this.myPublicProperty);
}
}
})();
jQuery

下面的示例中,定义了library函数,它声明一个新库,并在创建新库(即模块)时将init函数自动绑定到document.ready

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function library(module) {
$(function(){
if(module.init){
module.init();
}
});
return module;
}

var myLibrary = library(function() {
return {
init: function(){
// 模块实现
}
}
})();
优点
  1. 相比真正的封装思想,他对于很多拥有面向对象背景的开发人员来说更加整洁
  2. 支持私有数据
    缺点
  3. 无法为私有成员创建自动化单元测试
  4. bug修正补丁时会增加额外的复杂性,为私有方法打补丁时不可能的
  5. 开发人员无法轻易地扩展私有方法