JavaScript 模块化编程 - Module Pattern

来源:http://my.oschina.net/chenzhiqiang/blog/129783#OSC_h2_1

http://yuiblog.com/blog/2007/06/12/module-pattern/

http://benalman.com/news/2010/11/immediately-invoked-function-expression/

目录[-]

  • ## 前言
  • ## 模块模式
  • ### 闭包与 IIFE (Immediately-Invoked Function Expression)
  • ### 参数输入
  • ### 模块输出(Module Export)
  • #### 输出简单对象
  • #### 输出函数
  • ### Revealing Module Pattern
  • ## 模块模式的变化
  • ### 扩展
  • ### 松耦合扩展(Loose Augmentation)
  • ### 紧耦合扩展(Tight Augmentation)
  • ### 克隆与继承(Cloning and Inheritance)
  • ### 子模块(Sub-modules)
  • ### 各种形式的混合
  • ## 与其它模块规范或 JS 库的适配
  • ### 模块环境探测
  • ### 其它一些 JS 库的做法
  • ## JavaScript 模块化的未来
  • ## 参考资料

## 前言

The Module Pattern,模块模式,也译为模组模式,是一种通用的对代码进行模块化组织与定义的方式。这里所说的模块(Modules),是指实现某特定功能的一组方法和代码。许多现代语言都定义了代码的模块化组织方式,比如 Golang 和 Java,它们都使用 package 与 import 来管理与使用模块,而目前版本的 JavaScript 并未提供一种原生的、语言级别的模块化组织模式,而是将模块化的方法交由开发者来实现。因此,出现了很多种 JavaScript 模块化的实现方式,比如,CommonJS Modules、AMD 等。

以 AMD 为例,该规范使用 define 函数来定义模块。使用 AMD 规范进行模块化编程是很简单的,大致上的结构是这样的:

define(factory(){
  // 模块代码
  // return something;
});

目前尚在制定中的 Harmony/ECMAScript 6(也称为 ES.next),会对模块作出语言级别的定义,但距离实用尚遥不可及,这里暂时不讨论它。

作为一种模式,模块模式其实一直伴随着 JavaScript 存在,与 ES 6 无关。最近我需要重构自己的一些代码,因此我参考和总结了一些实用的模块化编程实践,以便更好的组织我的代码。需要注意的是,本文只是个人的一个总结,比较简单和片面,详尽的内容与剖析请参看文后的参考资料,它们写得很好。本文并不关心模块如何载入,只关心现今该如何组织模块化的代码。还有,不必过于纠结所谓的模式,真正重要的其实还是模块代码及思想。所谓模式,不过是我们书写代码的一些技巧和经验的总结,是一些惯用法,实践中应灵活运用。

## 模块模式

### 闭包与 IIFE (Immediately-Invoked Function Expression)

模块模式使用了 JavaScript 的一个特性,即闭包(Closures)。现今流行的一些 JS 库中经常见到以下形式的代码:

;(function (参数) {
  // 模块代码
  // return something;
})(参数);

上面的代码定义了一个匿名函数,并立即调用自己,这叫做自调用匿名函数(SIAF),更准确一点,称为立即调用的函数表达 (Immediately-Invoked Function Expression, IIFE–读做“iffy”)。

在闭包中,可以定义私有变量和函数,外部无法访问它们,从而做到了私有成员的隐藏和隔离。而通过返回对象或函数,或是将某对象作为参数传入,在函数体内对该对象进行操作,就可以公开我们所希望对外暴露的公开的方法与数据。

这,其实就是模块模式的本质。

注1:上面的代码中,最后的一对括号是对匿名函数的调用,因此必不可少。而前面的一对围绕着函数表达式的一对括号并不是必需的,但它可以用来给开发人员一个指示 -- 这是一个 IIFE。也有一些开发者在函数表达式前面加上一个惊叹号(!)或分号(;),而不是用括号包起来。比如 knockoutjs 的源码大致就是这样的:

!function (参数) {
  // 代码
  // return something
}(参数);

 

还有些人喜欢用括号将整个 IIFE 围起来,这样就变成了以下的形式:

(function (参数) {
  // 代码
  // return something
}(参数));

注2:在有些人的代码中,将 undefined 作为上面代码中的一个参数,他们那样做是因为 undefined 并不是 JavaScript 的保留字,用户也可以定义它,这样,当判断某个值是否是 undefined 的时候,判断可能会是错误的。将 undefined 作为一个参数传入,是希望代码能按预期那样运行。不过我认为,一般情况下那样做并没太大意义。

### 参数输入

JavaScript 有一个特性叫做隐式全局变量(implied globals),当使用一个变量名时,JavaScript 解释器将反向遍历作用域链来查找变量的声明,如果没有找到,就假定该变量是全局变量。这种特性使得我们可以在闭包里随处引用全局变量,比如 jQuery 或 window。然而,这是一种不好的方式。

考虑模块的独立性和封装,对其它对象的引用应该通过参数来引入。如果模块内需要使用其它全局对象,应该将这些对象作为参数来显式引用它们,而非在模块内直接引用这些对象的名字。以 jQuery 为例,若在参数中没有输入 jQuery 对象就在模块内直接引用 $ 这个对象,是有出错的可能的。正确的方式大致应该是这样的:

;(function (q, w) {
  // q is jQuery
  // w is window
  // 局部变量及代码
  // 返回
})(jQuery, window);

 相比隐式全局变量,将引用的对象作为参数,使它们得以和函数内的其它局部变量区分开来。这样做还有个好处,我们可以给那些全局对象起一个别名,比如上例中的 "q"。现在看看你的代码,是否没有经过对 jQuery 的引用就到处都是"$"?

### 模块输出(Module Export)

有时我们不只是要使用全局变量,我们也要声明和输出模块中的对象,这可以通过匿名函数的 return 语句来达成,而这也构成了一个完整的模块模式。来看一个完整的例子:

var MODULE = (function () {
    var my = {},
        privateVariable = 1;

    function privateMethod() {
        // ...
    }

    my.moduleProperty = 1;
    my.moduleMethod = function () {
        // ...
    };

    return my;
}());

这段代码声明了一个变量 MODULE,它带有两个可访问的属性:moduleProperty 和 moduleMethod,其它的代码都封装在闭包中保持着私有状态。参考以前提过的参数输入,我们还可以通过参数引用其它全局变量。

#### 输出简单对象

很多时候我们 return 一个对象作为模块的输出,比如上例就是。

另外,使用对象直接量(Object Literal Notation)来表达 JavaScript 对象是很常见的。比如:var x = { p1: 1, p2: "2", f: function(){ /*... */ } }

很多时候我们都能见到这样的模块化代码:

var Module1 = (function () {
  var private_variable = 1;
  function private_method() { /*...*/ }

  var my = {
    property1: 1,
    property2: private_variable,
    method1: private_method,
    method2: function () {
        // ...
    }
  };
  return my;
}());

另外,对于简单的模块化代码,若不涉及私有成员等,其实也可以直接使用对象直接量来表达一个模块:

var Widget1 = {
  name: "who am i?",
  settings: {
    x: 0,
    y: 0
  },
  call_me: function () {
    // ...
  }
};

有一篇文章讲解了这种形式: How Do You Structure JavaScript? The Module Pattern Edition

不过这只是一种简单的形式,你可以将它看作是模块模式的一种基础的简单表达形式,而把闭包形式看作是对它的一个封装。

#### 输出函数

有时候我们希望返回的并不是一个对象,而是一个函数。有两种需求要求我们返回一个函数,一种情况是我们需要它是一个函数,比如 jQuery,它是一个函数而不是一个简单对象;另一种情况是我们需要的是一个“类”而不是一个直接量,之后我们可以用 "new" 来实例它。目前版本的 JavaScript 并没有专门的“类”定义,但它却可以通过 function 来表达。

var Cat = (function () {
  // 私有成员及代码 ...

  return function(name) {
    this.name = name;
    this.bark = function() { /*...*/ }
  };
}());

var tomcat = new Cat("Tom");
tomcat.bark();

为什么不直接定义一个 function 而要把它放在闭包里呢?简单点的情况,确实不需要使用 IIFE 这种形式,但复杂点的情况,在构造我们所需要的函数或是“类”时,若需要定义一些私有的函数,就有必要使用 IIFE 这种形式了。

另外,在 ECMAScript 第五版中,提出了 Object.create() 方法。这时可以将一个对象视作“类”,并使用 Object.create() 进行实例化,不需使用 "new"。

### Revealing Module Pattern

前面已经提到一种形式是输出对象直接量(Object Literal Notation),而 Revealing Module Pattern 其实就是这种形式,只是做了一些限定。这种模式要求在私有范围内中定义变量和函数,然后返回一个匿名对象,在该对象中指定要公开的成员。参见下面的代码:

var MODULE = (function () {
  // 私有变量及函数
  var x = 1;
  function f1() {}
  function f2() {}

  return {
    public_method1: f1,
    public_method2: f2
  };
}());

## 模块模式的变化

### 扩展

上面的举例都是在一个地方定义模块,如果我们需要在数个文件中分别编写一个模块的不同部分该怎么办呢?或者说,如果我们需要对已有的模块作出扩展该怎么办呢?其实也很简单,将模块对象作为参数输入,扩展后再返回自己就可以了。比如:

var MODULE = (function (my) {
  my.anotherMethod = function () {
    // added method...
  };

  return my;
}(MODULE));

上面的代码为对象 MODULE 增加了一个 "anotherMethod" 方法。

### 松耦合扩展(Loose Augmentation)

上面的代码要求 MODULE 对象是已经定义过的。如果这个模块的各个组成部分并没有加载顺序要求的话,其实可以允许输入的参数为空对象,那么我们将上例中的参数由 MODULE 改为 MODULE || {} 就可以了:

var MODULE = (function (my) {
  // add capabilities...
  return my;
}(MODULE || {}));

### 紧耦合扩展(Tight Augmentation)

与上例不同,有时我们要求在扩展时调用以前已被定义的方法,这也有可能被用于覆盖已有的方法。这时,对模块的定义顺序是有要求的。

var MODULE = (function (my) {
  var old_moduleMethod = my.moduleMethod;

  my.moduleMethod = function () {
    // 方法重载
    // 可通过 old_moduleMethod 调用以前的方法...
  };

  return my;
}(MODULE));

### 克隆与继承(Cloning and Inheritance)

var MODULE_TWO = (function (old) {
    var my = {},
        key;

    for (key in old) {
        if (old.hasOwnProperty(key)) {
            my[key] = old[key];
        }
    }

    var super_moduleMethod = old.moduleMethod;
    my.moduleMethod = function () {
        // override method on the clone, access to super through super_moduleMethod
    };

    return my;
}(MODULE));

有时我们需要复制和继承原对象,上面的代码演示了这种操作,但未必完美。如果你可以使用 Object.create() 的话,请使用 Object.create() 来改写上面的代码:

var MODULE_TWO = (function (old) {
  var my = Object.create(old);

  var super_moduleMethod = old.moduleMethod;
  my.moduleMethod = function () {
    // override method ...
  };

  return my;
}(MODULE));

### 子模块(Sub-modules)

模块对象当然可以再包含子模块,形如 MODULE.Sub=(function(){}()) 之类,这里不再展开叙述了。

### 各种形式的混合

以上介绍了常见的几种模块化形式,实际应用中有可能是这些形式的混合体。比如:

var UTIL = (function (parent, $) {
    var my = parent.ajax = parent.ajax || {};

    my.get = function (url, params, callback) {
        // ok, so I'm cheating a bit :)
        return $.getJSON(url, params, callback);
    };

    // etc...

    return parent;
}(UTIL || {}, jQuery));

## 与其它模块规范或 JS 库的适配

### 模块环境探测

现今,CommonJS Modules 与 AMD 有着广泛的应用,如果确定 AMD 的 define 是可用的,我们当然可以使用 define 来编写模块化的代码。然而,我们不能假定我们的代码必然运行于 AMD 环境下。有没有办法可以让我们的代码既兼容于 CommonJS Modules 或 AMD 规范,又能在一般环境下运行呢?

其实我们只需要在某个地方加上对 CommonJS Modules 与 AMD 的探测并根据探测结果来“注册”自己就可以了,以上那些模块模式仍然有用。

AMD 定义了 define 函数,我们可以使用 typeof 探测该函数是否已定义。若要更严格一点,可以继续判断 define.amd 是否有定义。另外,SeaJS 也使用了 define 函数,但和 AMD 的 define 又不太一样。

对于 CommonJS,可以检查 exports 或是 module.exports 是否有定义。

现在,我写一个比较直白的例子来展示这个过程:

var MODULE = (function () {
  var my = {};
  // 代码 ...

  if (typeof define == 'function') {
    define( function(){ return my; } );
  }else if (typeof module != 'undefined' && module.exports) {
    module.exports = my;
  }
  return my;
}());

上面的代码在返回 my 对象之前,先检测自己是否是运行在 AMD 环境之中(检测 define 函数是否有定义),如果是,就使用 define 来定义模块,否则,继续检测是否运行于 CommonJS 中,比如 NodeJS,如果是,则将 my 赋值给 module.exports。因此,这段代码应该可以同时运行于 AMD、CommonJS 以及一般的环境之中。另外,我们的这种写法应该也可在 SeaJS 中正确执行。

### 其它一些 JS 库的做法

现在许多 JS 库都加入了对 AMD 或 CommonJS Modules 的适应,比如 jQuery, Mustache, doT, Juicer 等。

jQuery 的写法可参考 exports.js:

if ( typeof module === "object" && module && typeof module.exports === "object" ) {
    module.exports = jQuery;
} else {
    if ( typeof define === "function" && define.amd ) {
        define( "jquery", [], function () { return jQuery; } );
    }
}

if ( typeof window === "object" && typeof window.document === "object" ) {
    window.jQuery = window.$ = jQuery;
}

与前面我写的那段代码有些不同,在对 AMD 和 CommonJS 探测之后,它将 jQuery 注册成了 window 对象的成员。

然而,jQuery 是一个浏览器端的 JS 库,它那样写当然没问题。但如果我们所写的是一个通用的库,就不应使用 window 对象了,而应该使用全局对象,而这一般可以使用 this 来得到。

我们看看 Mustache 是怎么做的:

(function (root, factory) {
  if (typeof exports === "object" && exports) {
    factory(exports); // CommonJS
  } else {
    var mustache = {};
    factory(mustache);
    if (typeof define === "function" && define.amd) {
      define(mustache); // AMD
    } else {
      root.Mustache = mustache; // 
                    
                    

你可能感兴趣的:(JavaScript 模块化编程 - Module Pattern)