初涉JavaScript模式 (11) : 模块模式
引子
这篇算是对第9篇中内容的发散和补充,当时我只是把模块模式中的一些内容简单的归为函数篇中去,在北川的提醒下,我才发觉这是非常不严谨的,于是我把这些内容拎出来,这就是这篇的由来。
什么是模块模式
在JavaScript中没有包(Package)的概念,而面对日益庞大的JavaScript代码,而这正促使模块化开发的迫切需求,所以也就诞生了JavaScript的模块模式, 最早这么叫的是老道,他称之为 模块模式 (Module Patterns).
模块模式提供了用于创建独立解耦的代码片段的工具,这些代码可以被当成黑盒,当你正在写的软件需求发生变化时,这些代码可以被添加、替换、移除。
模块模式是好几种模式的组合,他包括
- 命名空间模式
- 依赖声明模式
- 私有和特权成员模式
- 即时函数模式
下面我将结合例子来展开他的每个组成模式。
命名空间模式
模块化的目标是支持大规模的程序开发,处理分散代码块的组装,并能让代码正确的运行,哪怕包含了我们不期望出现的模块代码,也能正确的执行代码。 为了做到这点,不同的模块必须避免修改全局执行上下文,因此后续模块应当在他们所期望的作用域内执行。这实际上意味着模块应竟可能少的定义全局标识,理想情况是,所有的模块都定义在一个全局标识内。而命名空间就是一种非常好的解决方案。
JavaScript 中没有命名空间的概念,但是这种特征是非常好实现的。可以创建一个全局对象,然后将所有的功能模块添加到该全局对象中去,从而在具有大量函数、对象和其他变量的情况下不会污染全局范围。
随着功能模块的增加,如下所示:
```javascript //添加一个模块 var module = module || {}; //在module里面添加一个模块 if (!typeof module === "undefined") { module.module1 = {}; } else { var module = {}; module.module1 = {}; } //在module1里面添加一个模块 if (!typeof module === "undefined") { if (!typeof module.module1 === "undefined") { module.module1.module2 = {}; } else { module.module1 = {}; module.module1.module2 = {}; } } else { var module = {}; module.module1 = {}; module.module1.module2 = {}; } ```
我们发现每次添加新的模块前我们都要去检查这些模块是否已经存在(否则可能覆盖),以至于产生了大量的重复代码。所以这时我们需要一个很方便的处理命名空间细节的可重用函数,代码如下:
```javascript //定义一个全局标识 var GLOBAL = GLOBAL || {}; //处理命名空间的函数 GLOBAL.namespace = function(str) { var arr = str.split("."), o = GLOBAL,i;
for (i = (arr[0] == "GLOBAL") ? 1 : 0; i < arr.length; i++) { o[arr[i]] = o[arr[i]] || {}; o = o[arr[i]]; } } //注册一个模块(即给一个模块划分一个命名空间) GLOBAL.namespace("Module1"); //给Module1添加一个print方法 GLOBAL.Module1.print = function(msg){ console.log(msg); } //调用Module1的print函数 GLOBAL.Module1.print("1"); //1 //也可以这样调用 GLOBAL.Module1.print.call(null, "3"); //3 ```
这个实现是非破坏性的,也就是说,如果这个属性已经存在,则不会再重新创建。我们只需要在每次创建模块前先注册下,而不需要去管他是否存在,这样一来大大减少了代码量。
声明依赖
这和Java中的依赖注入很像,一个功能类不可能单独存在,他需要一些“基础设施”,例如我们要吃核桃,我们需要一个工具去打开它,而这个工具就是我们吃核桃的基础设施,也叫做依赖。在程序中,我们做的通常是导入其他的支持模块也就是依赖,从而避免了我们重复去写那些简单的基础逻辑。
在其他语言中,都有专门的语法结构去实现,而在JavaScript中,我们也可以简单的自己去规范。代码如下:
```javascript GLOBAL.namespace("Nut"); //给“核桃” 添加一个吃的方法 GLOBAL.Nut.eat = function(aNut){ //依赖 var tools = GLOBAL.Tools.spanner; //用扳子砸核桃 tools.use.call(aNut); } ```
这是一段没有实际意义的代码,只是用来说明依赖,其中的扳子就是依赖,我们使用他来吃核桃,在程序中,我们就要导入这个“板子”。
虽然很简单,但是这样做有很多好处:
- 在函数顶部的声明可以让我们很容易发现并解析依赖
- 解析局部变量的速度总比解析全局变量要快
- 某些压缩工具可以对其进行简化,减少了字节数(工具会对局部变量进行简化,而全局变量则不可以)
私有和特权成员
在其他oo语言中,都有类似private,public,interface这样的语法来区别公共属性,私有属性,公共接口。但是JavaScript没有(都要我们自己去实现,这也是我热爱JS的原因之一)。在JavaScript中所有对象的成员都是公开的。在JavaScript中:
```javascript //创建一个对象 var WeiRan = { name: "WeiRan", getName: function() { console.log(this.name); } } //访问这个对象 console.log(WeiRan.name); //WeiRan WeiRan.getName(); //WeiRan //通过构造函数来创建对象 function Person(name) { this.name = name; this.getName = function() { console.log(this.name); } } //实例化一个对象 var wr = new Person("WeiRan"); //访问这个实例 console.log(wr.name); //WeiRan wr.getName(); //WeiRan ```
我们发现无论我们是通过字面量的方式还是构造函数的方式创建对象,新建对象的所有属性和方法对外都是公开的,但是在很多场景下,我们并不想把所有的逻辑都暴露给调用者,这也就是说,我们需要私有的属性或方法,虽然JavaScript中没有针对私有成员的特俗语法,但是我们可以使用闭包来实现这种功能。代码如下:
```javascript function Person(UserName) { //私有属性 var name = UserName; //API this.getName = function() { console.log(name); } } //实例化一个对象 var wr = new Person("WeiRan"); //访问对象 console.log(wr.name); //undefined wr.getName(); //WeiRan ```
我们知道,局部变量只有在特定的作用域下才能访问到,比如Person方法内部定义的局部变量name只有在Person内才能访问,这恰好符合了我们私有成员的要求,我们通过闭包对外公开了一个借口(getName),外部想要访问wr的name只能通过getName去访问。而类似getName这样对外公布的方法我们称之为特权方法(Privileged Method),我更喜欢叫他对外接口。
特权方法特别要注意的是,为了保证整个方法的私有成员,不要返回对整个方法的引用,这样外部也就能随便的通过这个引用来访问所有的私有成员,这也就没有任何私有性可言了。
即时函数
在上面的代码中,我们搭起了一个简单的模块架子,但是在通常的场景下,我们会遇到一些问题,实例如下:
```javascript //定义一个全局标识 var GLOBAL = GLOBAL || {}; //处理命名空间的函数 GLOBAL.namespace = function(str) { var arr = str.split("."), o = GLOBAL,
i; for (i = (arr[0] == "GLOBAL") ? 1 : 0; i < arr.length; i++) { o[arr[i]] = o[arr[i]] || {}; o = o[arr[i]]; } } //注册一个模块 GLOBAL.namespace("Module"); //字面量方式 GLOBAL.Module.console = { msg : "WeiRan", log : function(){ console.log(this.msg); } } GLOBAL.Module.console.log(); //WeiRan console.log(GLOBAL.Module.console.msg); //WeiRan //即时函数方式 GLOBAL.Module.console1 = (function(){ var msg = "WeiRan"; return{ log : function(){ console.log(msg); } } }()); GLOBAL.Module.console1.log(); //WeiRan console.log(GLOBAL.Module.console1.msg); //undefined ```
在上面我列举了两种编写模块的方式,第一种字面量的形式完全不能保证私有属性,他的所有成员都是公开的,第二种,我们通过即时函数提供的私有作用域保证了模块私有成员的私有性,在最后返回对象了一个对象,该对象包含该模块的公共API。
而对于返回的对象,如果我们还要对公共API的具体实现逻辑也保持私有,那么使用揭示模块模式就再合适不过了。
揭示模块模式
揭示模块模式是对模块模式中私有性保护的升级,就拿我以前的一段代码为例,详细代码如下:
```javascript GLOBAL.comm.comnav = (function() { var bindNav = function(navList) { $.each(navList, function(key, val) { $("#" + val.split("-")[0] + ">span").click(function() { $(this).siblings().each(function() { $(this).find(".active").hide().siblings().find("[_tag=" + $(this).attr("_tag") + "]").removeClass("red"); }); $(this).find(".active").show().siblings().find("[_tag=" + $(this).attr("_tag") + "]").addClass("red"); showTag([val.split("-")[1], val.split("-")[2]], $(this).attr("_tag")); }); }); }; var processNav = function(navId, commId, listId) { return navId + "-" + commId + "-" + listId; }; var showTag = function(tagList, tagName) { $.each(tagList, function(index) { $("#" + tagList[index]).find("[_tag=" + tagName + "]").show().siblings().hide(); }); }; return { init: bindNav, newInstantce: processNav } })(); ```
在上面的示例中,我在模块内部定义了三个私有方法(注意是以函数表达式的形式定义的),我没有把API函数的具体逻辑显式的写在返回的对象中,我是以函数名的方式传递了一个私有方法的引用给API函数,这样我们就连模块暴露的API方法都进行了处理,这样才算真正的接口(只有声明,没有实现)
结语
由于最近工作和过年的关系,断了好长时间,自己也意识到自己懈怠了,新的一年开始了,还有太多东西等着我去学,2014就是一个字,干!
如果你在文中发现错误或则你觉得不正确的地方,希望你的指正。