深入理解JavaScript系列(3) 全面解析Module模式

简介
Module模式是JavaScript编程中一个非常通用的模式,一般情况下,大家都知道基本用法,本文尝试着给大家更多该模式的高级使用方式。
首先我们来看看Module模式的基本特征:
模块化,可重用
封装了变量和function,和全局的namaspace不接触,松耦合
只暴露可用public的方法,其它私有方法全部隐藏
关于Module模式,最早是由YUI的成员Eric Miraglia在4年前提出了这个概念,我们将从一个简单的例子来解释一下基本的用法(如果你已经非常熟悉了,请忽略这一节)。
基本用法
先看一下最简单的一个实现,代码如下:


代码如下:

var Calculator = function (eq) {
//这里可以声明私有成员
var eqCtl = document.getElementById(eq);
return {
// 暴露公开的成员
add: function (x, y) {
var val = x + y;
eqCtl.innerHTML = val;
}
};
};

我们可以通过如下的方式来调用:


代码如下:

var calculator = new Calculator('eq');
calculator.add(2, 2);

大家可能看到了,每次用的时候都要new一下,也就是说每个实例在内存里都是一份copy,如果你不需要传参数或者没有一些特殊苛刻的要求的话,我们可以在最后一个}后面加上一个括号,来达到自执行的目的,这样该实例在内存中只会存在一份copy,不过在展示他的优点之前,我们还是先来看看这个模式的基本使用方法吧。
匿名闭包
匿名闭包是让一切成为可能的基础,而这也是JavaScript最好的特性,我们来创建一个最简单的闭包函数,函数内部的代码一直存在于闭包内,在整个运行周期内,该闭包都保证了内部的代码处于私有状态。


代码如下:

(function () {
// ... 所有的变量和function都在这里声明,并且作用域也只能在这个匿名闭包里
// ...但是这里的代码依然可以访问外部全局的对象
}());

注意,匿名函数后面的括号,这是JavaScript语言所要求的,因为如果你不声明的话,JavaScript解释器默认是声明一个function函数,有括号,就是创建一个函数表达式,也就是自执行,用的时候不用和上面那样在new了,当然你也可以这样来声明:
(function () {/* 内部代码 */})();
不过我们推荐使用第一种方式,关于函数自执行,我后面会有专门一篇文章进行详解,这里就不多说了。
引用全局变量
JavaScript有一个特性叫做隐式全局变量,不管一个变量有没有用过,JavaScript解释器反向遍历作用域链来查找整个变量的var声明,如果没有找到var,解释器则假定该变量是全局变量,如果该变量用于了赋值操作的话,之前如果不存在的话,解释器则会自动创建它,这就是说在匿名闭包里使用或创建全局变量非常容易,不过比较困难的是,代码比较难管理,尤其是阅读代码的人看着很多区分哪些变量是全局的,哪些是局部的。
不过,好在在匿名函数里我们可以提供一个比较简单的替代方案,我们可以将全局变量当成一个参数传入到匿名函数然后使用,相比隐式全局变量,它又清晰又快,我们来看一个例子:


代码如下:

(function ($, YAHOO) {
// 这里,我们的代码就可以使用全局的jQuery对象了,YAHOO也是一样
} (jQuery, YAHOO));

现在很多类库里都有这种使用方式,比如jQuery源码。
不过,有时候可能不仅仅要使用全局变量,而是也想声明全局变量,如何做呢?我们可以通过匿名函数的返回值来返回这个全局变量,这也就是一个基本的Module模式,来看一个完整的代码:


代码如下:

var blogModule = (function () {
var my = {}, privateName = "博客园";
function privateAddTopic(data) {
// 这里是内部处理代码
}
my.Name = privateName;
my.AddTopic = function (data) {
privateAddTopic(data);
};
return my;
} ());

上面的代码声明了一个全局变量blogModule,并且带有2个可访问的属性:blogModule.AddTopic和blogModule.Name,除此之外,其它代码都在匿名函数的闭包里保持着私有状态。同时根据上面传入全局变量的例子,我们也可以很方便地传入其它的全局变量。
高级用法
上面的内容对大多数用户已经很足够了,但我们还可以基于此模式延伸出更强大,易于扩展的结构,让我们一个一个来看。
扩展
Module模式的一个限制就是所有的代码都要写在一个文件,但是在一些大型项目里,将一个功能分离成多个文件是非常重要的,因为可以多人合作易于开发。再回头看看上面的全局参数导入例子,我们能否把blogModule自身传进去呢?答案是肯定的,我们先将blogModule传进去,添加一个函数属性,然后再返回就达到了我们所说的目的,上代码:


代码如下:

var blogModule = (function (my) {
my.AddPhoto = function () {
//添加内部代码
};
return my;
} (blogModule));

这段代码,看起来是不是有C#里扩展方法的感觉?有点类似,但本质不一样哦。同时尽管var不是必须的,但为了确保一致,我们再次使用了它,代码执行以后,blogModule下的AddPhoto就可以使用了,同时匿名函数内部的代码也依然保证了私密性和内部状态。
松耦合扩展
上面的代码尽管可以执行,但是必须先声明blogModule,然后再执行上面的扩展代码,也就是说步骤不能乱,怎么解决这个问题呢?我们来回想一下,我们平时声明变量的都是都是这样的:
var cnblogs = cnblogs || {} ;
这是确保cnblogs对象,在存在的时候直接用,不存在的时候直接赋值为{},我们来看看如何利用这个特性来实现Module模式的任意加载顺序:


代码如下:

var blogModule = (function (my) {
// 添加一些功能
return my;
} (blogModule || {}));

通过这样的代码,每个单独分离的文件都保证这个结构,那么我们就可以实现任意顺序的加载,所以,这个时候的var就是必须要声明的,因为不声明,其它文件读取不到哦。
紧耦合扩展
虽然松耦合扩展很牛叉了,但是可能也会存在一些限制,比如你没办法重写你的一些属性或者函数,也不能在初始化的时候就是用Module的属性。紧耦合扩展限制了加载顺序,但是提供了我们重载的机会,看如下例子:


代码如下:

var blogModule = (function (my) {
var oldAddPhotoMethod = my.AddPhoto;
my.AddPhoto = function () {
// 重载方法,依然可通过oldAddPhotoMethod调用旧的方法
};
return my;
} (blogModule));

通过这种方式,我们达到了重载的目的,当然如果你想在继续在内部使用原有的属性,你可以调用oldAddPhotoMethod来用。
克隆与继承


代码如下:

var blogModule = (function (old) {
var my = {},
key;
for (key in old) {
if (old.hasOwnProperty(key)) {
my[key] = old[key];
}
}
var oldAddPhotoMethod = old.AddPhoto;
my.AddPhoto = function () {
// 克隆以后,进行了重写,当然也可以继续调用oldAddPhotoMethod
};
return my;
} (blogModule));

这种方式灵活是灵活,但是也需要花费灵活的代价,其实该对象的属性对象或function根本没有被复制,只是对同一个对象多了一种引用而已,所以如果老对象去改变它,那克隆以后的对象所拥有的属性或function函数也会被改变,解决这个问题,我们就得是用递归,但递归对function函数的赋值也不好用,所以我们在递归的时候eval相应的function。不管怎么样,我还是把这一个方式放在这个帖子里了,大家使用的时候注意一下就行了。
跨文件共享私有对象
通过上面的例子,我们知道,如果一个module分割到多个文件的话,每个文件需要保证一样的结构,也就是说每个文件匿名函数里的私有对象都不能交叉访问,那如果我们非要使用,那怎么办呢? 我们先看一段代码:


代码如下:

var blogModule = (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;
};
return my;
} (blogModule || {}));

任何文件都可以对他们的局部变量_private设属性,并且设置对其他的文件也立即生效。一旦这个模块加载结束,应用会调用 blogModule._seal()"上锁",这会阻止外部接入内部的_private。如果这个模块需要再次增生,应用的生命周期内,任何文件都可以调用_unseal() ”开锁”,然后再加载新文件。加载后再次调用 _seal()”上锁”。
子模块
最后一个也是最简单的使用方式,那就是创建子模块


代码如下:

blogModule.CommentSubModule = (function () {
var my = {};
// ...
return my;
} ());

尽管非常简单,我还是把它放进来了,因为我想说明的是子模块也具有一般模块所有的高级使用方式,也就是说你可以对任意子模块再次使用上面的一些应用方法。
总结
上面的大部分方式都可以互相组合使用的,一般来说如果要设计系统,可能会用到松耦合扩展,私有状态和子模块这样的方式。另外,我这里没有提到性能问题,但我认为Module模式效率高,代码少,加载速度快。使用松耦合扩展允许并行加载,这更可以提升下载速度。不过初始化时间可能要慢一些,但是为了使用好的模式,这是值得的。
参考文章:
http://yuiblog.com/blog/2007/06/12/module-pattern/
http://www.adequatelygood.com/2010/3/JavaScript-Module-Pattern-In-Depth
同步与推荐
本文已同步至目录索引:深入理解JavaScript系列
深入理解JavaScript系列文章,包括了原创,翻译,转载等各类型的文章,如果对你有用,请推荐支持一把,给大叔写作的动力。

(0)

相关推荐

  • Javarscript中模块(module)、加载(load)与捆绑(bundle)详解

    JS模块简介 js模块化,简单说就是将系统或者功能分隔成单独的.互不影响的代码片段,经过严格定义接口,使各模块间互不影响,且可以为其他所用. 常见的模块化有,C中的include (.h)文件.java中的import等. 为什么JS需要模块 很显然,没有模块我们也可以实现同样的功能,为什么我们还要使用模块来写js代码呢?下面几点是模块化给我们带来的一些变化: 抽象代码:我们在使用模块来调用一个api时,可以不用知道内部是如何实现的,避免去理解其中复杂的代码: 封装代码:在不需要再次修改代码的前

  • JavaScript的Module模式编程深入分析

    基础知识 首先我们要大概了解一下Module模式(2007年由YUI的EricMiraglia在博客中提出),如果你已熟悉 Module 模式,可以跳过本部分,直接阅读"高级模式". 匿名函数闭包 匿名函数闭包是JavaScript最棒的特征,没有之一,是它让一切都成为了可能.现在我们来创建一个匿名函数然后立即执行.函数中所有的代码都是在一个闭包中执行的,闭包决定了在整个执行过程中这些代码的私有性和状态. 复制代码 代码如下: (function () { // ... all var

  • 深入理解JavaScript系列(3) 全面解析Module模式

    简介 Module模式是JavaScript编程中一个非常通用的模式,一般情况下,大家都知道基本用法,本文尝试着给大家更多该模式的高级使用方式. 首先我们来看看Module模式的基本特征: 模块化,可重用 封装了变量和function,和全局的namaspace不接触,松耦合 只暴露可用public的方法,其它私有方法全部隐藏 关于Module模式,最早是由YUI的成员Eric Miraglia在4年前提出了这个概念,我们将从一个简单的例子来解释一下基本的用法(如果你已经非常熟悉了,请忽略这一节

  • 深入理解JavaScript系列(4) 立即调用的函数表达式

    前言 大家学JavaScript的时候,经常遇到自执行匿名函数的代码,今天我们主要就来想想说一下自执行. 在详细了解这个之前,我们来谈了解一下"自执行"这个叫法,本文对这个功能的叫法也不一定完全对,主要是看个人如何理解,因为有的人说立即调用,有的人说自动执行,所以你完全可以按照你自己的理解来取一个名字,不过我听很多人都叫它为"自执行",但作者后面说了很多,来说服大家称呼为"立即调用的函数表达式". 本文英文原文地址:http://benalman

  • 深入理解JavaScript系列(47):对象创建模式(上篇)

    介绍 本篇主要是介绍创建对象方面的模式,利用各种技巧可以极大地避免了错误或者可以编写出非常精简的代码. 模式1:命名空间(namespace) 命名空间可以减少全局命名所需的数量,避免命名冲突或过度.一般我们在进行对象层级定义的时候,经常是这样的: 复制代码 代码如下: var app = app || {}; app.moduleA = app.moduleA || {}; app.moduleA.subModule = app.moduleA.subModule || {}; app.mod

  • 深入理解JavaScript系列(1) 编写高质量JavaScript代码的基本要点

    具体一点就是编写高质量JavaScript的一些要素,例如避免全局变量,使用单变量声明,在循环中预缓存length(长度),遵循代码阅读,以及更多. 此摘要也包括一些与代码不太相关的习惯,但对整体代码的创建息息相关,包括撰写API文档.执行同行评审以及运行JSLint.这些习惯和最佳做法可以帮助你写出更好的,更易于理解和维护的代码,这些代码在几个月或是几年之后再回过头看看也是会觉得很自豪的. 书写可维护的代码(Writing Maintainable Code ) 软件bug的修复是昂贵的,并且

  • 深入理解JavaScript系列(2) 揭秘命名函数表达式

    前言 网上还没用发现有人对命名函数表达式进去重复深入的讨论,正因为如此,网上出现了各种各样的误解,本文将从原理和实践两个方面来探讨JavaScript关于命名函数表达式的优缺点. 简单的说,命名函数表达式只有一个用户,那就是在Debug或者Profiler分析的时候来描述函数的名称,也可以使用函数名实现递归,但很快你就会发现其实是不切实际的.当然,如果你不关注调试,那就没什么可担心的了,否则,如果你想了解兼容性方面的东西的话,你还是应该继续往下看看. 我们先开始看看,什么叫函数表达式,然后再说一

  • 深入理解JavaScript系列(12) 变量对象(Variable Object)

    JavaScript编程的时候总避免不了声明函数和变量,以成功构建我们的系统,但是解释器是如何并且在什么地方去查找这些函数和变量呢?我们引用这些对象的时候究竟发生了什么? 原始发布:Dmitry A. Soshnikov 发布时间:2009-06-27 俄文地址:http://dmitrysoshnikov.com/ecmascript/ru-chapter-2-variable-object/ 英文翻译:Dmitry A. Soshnikov 发布时间:2010-03-15 英文地址:http

  • 深入理解JavaScript中的预解析

    前言 JavaScript是解释型语言是毋庸置疑的,但它是不是仅在运行时自上往下一句一句地解析的呢? 事实上或某种现象证明并不是这样的,通过<JavaScript权威指南>及网上相关资料了解到,JavaScript有"预解析"行为.理解这一特性是很重要的,不然在实际开发中你可能会遇到很多无从解析的问题,甚至导致程序bug的存在.为了解析这一现象,也作为自己的一次学习总结,本文逐步引导你来认识JavaScript"预解析",如果我的见解有误,还望指正. 在

  • 深入理解JavaScript系列(16) 闭包(Closures)

    介绍 本章我们将介绍在JavaScript里大家经常来讨论的话题 -- 闭包(closure).闭包其实大家都已经谈烂了.尽管如此,这里还是要试着从理论角度来讨论下闭包,看看ECMAScript中的闭包内部究竟是如何工作的. 正如在前面的文章中提到的,这些文章都是系列文章,相互之间都是有关联的.因此,为了更好的理解本文要介绍的内容,建议先去阅读第14章作用域链和第12章变量对象. 英文原文:http://dmitrysoshnikov.com/ecmascript/chapter-6-closu

  • 深入理解JavaScript系列(19):求值策略(Evaluation strategy)详解

    介绍 本章,我们将讲解在ECMAScript向函数function传递参数的策略. 计算机科学里对这种策略一般称为"evaluation strategy"(大叔注:有的人说翻译成求值策略,有的人翻译成赋值策略,通看下面的内容,我觉得称为赋值策略更为恰当,anyway,标题还是写成大家容易理解的求值策略吧),例如在编程语言为求值或者计算表达式设置规则.向函数传递参数的策略是一个特殊的case. http://dmitrysoshnikov.com/ecmascript/chapter-

  • 深入理解JavaScript系列(18):面向对象编程之ECMAScript实现

    介绍 本章是关于ECMAScript面向对象实现的第2篇,第1篇我们讨论的是概论和CEMAScript的比较,如果你还没有读第1篇,在进行本章之前,我强烈建议你先读一下第1篇,因为本篇实在太长了(35页). 英文原文:http://dmitrysoshnikov.com/ecmascript/chapter-7-2-oop-ecmascript-implementation/ 注:由于篇幅太长了,难免出现错误,时刻保持修正中. 在概论里,我们延伸到了ECMAScript,现在,当我们知道它OOP

随机推荐