模块化简介

需要模块管理的原因就是JavaScript发展的越来越快,超过了它产生时候的自我定位。由于没有模块管理的概念,在做大型项目或者文件组织的时候,就会异常纠结。所以才会产生出这么多的模块管理工具。模块化的主要特征是:

  • 模块化,可重用
  • 封装了变量和function,和全局的namaspace不接触,松耦合
  • 只暴露可用public的方法,其它私有方法全部隐藏

    实现方式

低级写法

初级阶段,代码中充斥的是各种函数,通过名称区分:

  1. function func1() {}
  2. function func2() {}

实际上这些函数都被挂载到了全局变量下,使用的时候可以直接调用,缺点就是: "污染"了全局变量,无法保证不与其他模块发生变量名冲突,而且模块成员之间看不出直接关系。

对象写法

解决上述污染问题,可以把模块写成一个对象,所有的模块成员都放到这个对象里面。

  1. var module1 = new Object({
  2.     _count : 0,
  3.     m1 : function (){
  4.       //...
  5.     },
  6.     m2 : function (){
  7.       //...
  8.     }
  9.   });
  10. // 等价于
  11. var module2 = {
  12. _count: 0,
  13. m1 : function (){
  14.       //...
  15.     },
  16.   m2 : function (){
  17.     //...
  18.   }
  19. }

外部可以通过定义的变量名访问,起到了一点点的命名空间作用,但是同样缺点明显: 会暴露所有模块成员,内部状态可以被外部改写。比如,外部代码可以直接改变内_count的值。

立刻执行函数(Immediately-Invoked Function Expression,IIFE) or 匿名闭包

如果你不需要传参数或者没有一些特殊苛刻的要求的,可以使用立刻执行函数,该实例在内存中只会存在一份copy。

  1. var module1 = (function(){
  2.     var _count = 0;
  3.     var m1 = function(){
  4.       //...
  5.     };
  6.     var m2 = function(){
  7.       //...
  8.     };
  9.     return {
  10.       m1 : m1,
  11.       m2 : m2
  12.     };
  13.   })();

这样可以很好的保护私有变量,通过return来设置公开的方法。缺点也有: 动态添加方法的时候比较麻烦,且无法修改内部私有变量.

放大模式 or 宽放大模式(Loose augmentation)

如果一个模块很大,必须分成几个部分,或者一个模块需要继承另一个模块,这时就有必要采用"放大模式"(augmentation)。

  1. var module1 = (function (mod){
  2.     mod.m3 = function () {
  3.       //...
  4.     };
  5.     return mod;
  6.   })(module1);

在浏览器环境中,模块的各个部分通常都是从网上获取的,有时无法知道哪个部分会先加载。如果采用上一节的写法,第一个执行的部分有可能加载一个不存在空对象,这时就要采用"宽放大模式"。

  1. var module1 = ( function (mod){
  2.     //...
  3.     return mod;
  4.   })(window.module1 || {});

构造函数

使用new操作符完成的构造函数,比如:

  1. var Person = function(name) {
  2. var name = name || 'leo'; // 私有变量,通过闭包访问
  3. return {
  4. // 暴露公开的成员
  5. setName: function (name) {
  6. name = name;
  7. },
  8. getName: function () {
  9. return name;
  10. }
  11. };
  12. }
  13. var leo = new Person(); // Object{setName: function, getName: function()}
  14. console.log(leo.getName());

每次用的时候都要new一下,也就是说每个实例在内存里都是一份copy。

引用全局变量

JavaScript有一个特性叫做隐式全局变量,不管一个变量有没有用过,JavaScript解释器反向遍历作用域链来查找整个变量的var声明,如果没有找到var,解释器则假定该变量是全局变量,如果该变量用于了赋值操作的话,之前如果不存在的话,解释器则会自动创建它,这就是说在匿名闭包里使用或创建全局变量非常容易,不过比较困难的是,代码比较难管理,尤其是阅读代码的人看着很多区分哪些变量是全局的,哪些是局部的。

匿名函数里我们可以提供一个比较简单的替代方案,我们可以将全局变量当成一个参数传入到匿名函数然后使用,相比隐式全局变量,它又清晰又快,我们来看一个例子:

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

基本的Module模式

有时候可能不仅仅要使用全局变量,而是也想声明全局变量,如何做呢?我们可以通过匿名函数的返回值来返回这个全局变量,这也就是一个基本的Module模式,来看一个完整的代码:

  1. var blogModule = (function () {
  2. var my = {}, privateName = "博客园";
  3. function privateAddTopic(data) {
  4. // 这里是内部处理代码
  5. }
  6. my.Name = privateName;
  7. my.AddTopic = function (data) {
  8. privateAddTopic(data);
  9. };
  10. return my;
  11. } ());

上面的代码声明了一个全局变量blogModule,并且带有2个可访问的属性:blogModule.AddTopic和blogModule.Name,除此之外,其它代码都在匿名函数的闭包里保持着私有状态。同时根据上面传入全局变量的例子,我们也可以很方便地传入其它的全局变量。

Module扩展方式

Module模式的一个限制就是所有的代码都要写在一个文件,但是在一些大型项目里,将一个功能分离成多个文件是非常重要的,因为可以多人合作易于开发。再回头看看上面的全局参数导入例子,我们能否把blogModule自身传进去呢?答案是肯定的,我们先将blogModule传进去,添加一个函数属性,然后再返回就达到了我们所说的目的,上代码:

  1. var blogModule = (function (my) {
  2. my.AddPhoto = function () {
  3. //添加内部代码
  4. };
  5. return my;
  6. } (blogModule));

但是有一个问题,这里扩展的方法,好像无法调用函数内部的私有变量,但是可以将一些需要的私有变量挂载到对象下。

松耦合扩展

上面的代码尽管可以执行,但是必须先声明blogModule,然后再执行上面的扩展代码,也就是说步骤不能乱,怎么解决这个问题呢?我们来回想一下,我们平时声明变量的都是都是这样的:var cnblogs = cnblogs || {} ;,确保cnblogs对象,在存在的时候直接用,不存在的时候直接赋值为{},我们来看看如何利用这个特性来实现Module模式的任意加载顺序:

  1. var blogModule = (function (my) {
  2. // 添加一些功能
  3. return my;
  4. } (blogModule || {}));

这样每个单独分离的文件都保证这个结构,那么我们就可以实现任意顺序的加载,所以,这个时候的var就是必须要声明的,因为不声明,其它文件读取不到的。缺点: 没办法重写你的一些属性或者函数,也不能在初始化的时候就是用Module的属性。

紧耦合扩展

紧耦合扩展限制了加载顺序,但是提供了我们重载的机会,看如下例子:

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

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

跨文件共享私有对象

如果一个module分割到多个文件的话,每个文件需要保证一样的结构,也就是说每个文件匿名函数里的私有对象都不能交叉访问,那如果我们非要使用,那怎么办呢? 我们先看一段代码:

  1. var blogModule = (function (my) {
  2. var _private = my._private = my._private || {},
  3. _seal = my._seal = my._seal || function () {
  4. delete my._private;
  5. delete my._seal;
  6. delete my._unseal;
  7. },
  8. _unseal = my._unseal = my._unseal || function () {
  9. my._private = _private;
  10. my._seal = _seal;
  11. my._unseal = _unseal;
  12. };
  13. return my;
  14. } (blogModule || {}));

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

参考资料

原文: https://leohxj.gitbooks.io/front-end-database/content/javascript-modules/modules-intro.html