掌握模块模式

原文:Mastering the Module Pattern
翻译:dreamana.com

我是 JavaScript 模块模式的狂热份子。我想分享一些这个模式的用例和不同写法,以及说明为什么这些很重要。模块模式 (Module Pattern) 就是所谓的一种“设计模式”,由于各种原因它非常的有用。我之所以喜欢模块模式(它的变体, 揭示模块模式 Revealing Module Pattern)是因为它让域界定 (scoping) 轻而易举,不会使 JavaScript 设计变得过于复杂。

这也让代码保持简单,易于阅读和使用,使用 Object 是一种非常好的方式,不会让你的代码被啰嗦的 this 和 prototype 声明弄得臃肿。我觉得我应该去分享一些领悟,关于模块非常棒的一面,以及如何去掌握它和它的变体及特性。

创建一个模块 (Creating a Module)

要理解一个模块可以带来什么,你需要理解以下函数的概念是什么:

(function () {
  // code
})();

这声明了一个函数,然后立即自己调用。这也称为 立即调用的函数表达式 (Immediately-Invoked-Function-Expressions’s),其中这函数创建了新域并创建了“私有空间 (privacy)”,JavaScript 没有私有空间,但可以用创建新域,在里面包装我们所有函数逻辑来模拟这个。这个做法的结果是,只返回我们需要的部分,其他代码从全局域 (global scope) 分离出来。

创建新域之后,我们需要给我们的代码加命名空间 (namespace),使得可以访问我们返回的任何方法。现在给我们的匿名模块一个命名空间吧。

var Module = (function () {
  // code
})();

之后我们有 Module 声明在全局域内,这意味着能够随处调用,甚至传到另一个模块里面。

私有方法 (Private methods)

你可能听说过很多关于 JavaScript 的私有方法,但严格来说是没有这回事的,但是我们可以创造出来。

你可能会问,什么是私有方法?私有方法是你不想让用户/开发者/黑客看到/调用到他们所在域之外的东西。我们可能在做服务端调用发送敏感数据,我们不想暴露这些函数在外面,它们能够发送一些东西回来并利用我们的代码。因此我们可以创建闭包并更明智地保护我们的代码(用 JavaScript 尽可能地做到)。然而不是仅仅为了保护,还有命名冲突的问题。我猜你刚开始写 jQuery/JavaScript 的时候,你讲所有代码丢到一个文件里并且只是 function, function, function 这样写。很少知道这些都是全局的,在某些时候你也许会忍受这些结果。假如这样的话,你会想知道为什么,怎样去改变它。

那么,现在用我们新创建的 Module 域去做些外部不能访问的方法。对于模块模式的初学者,这个例子能帮助理解私有方法是怎样定义的:

var Module = (function () {

  var privateMethod = function () {
    // do something
  };

})();

上面的例子声明了函数 privateMethod,那是在新域内本地声明的。如果我们试图在模块外任何地方去调用它会得到一个错误抛出,JavaScript 程序崩溃!我们不想任何人调用这个方法,尤其有些人想操作数据转到背后传到某个服务器上。

理解 return (Understanding “return”)

通常模块会使用 return,返回一个对象 (Object) 到 Module,而 Object 中的方法可以从 Module 命名空间访问。

一个实际的精简例子返回一个带有函数属性的对象:

var Module = (function () {

  return {
    publicMethod: function () {
      // code
    }
  };

})();

一旦返回了对象字面量 (Object Literal),为什么就像对象字面量一样调用:

Module.publicMethod();

对于未使用过对象字面量的人,可以看看这个标准的对象字面量长得像这样子:

var myObjLiteral = {
  defaults: { name: 'Todd' },
  someMethod: function () {
    console.log(this.defaults);
  }
};

// console.log: Object { name: 'Todd' }
myObjLiteral.someMethod();

但对象字面量的问题 (issue) 是我们可以滥用那个模式,将“私有”方法栓在上面,作为对象的一部分,也可以被访问。这就是模块带给我们好处,允许我们本地定义私有的东西,只返回“好的部分”。

让我们更进一步探讨对象字面量语法,一个完美的模块模式以及返回关键词的角色。通常地模块会返回一个对象,但怎样定义和构造对象完全取决于你。根据项目以及代码的角色/设定,我会选用这些语法中的其中一种。

匿名对象字面量返回 (Anonymous Object Literal return)

最简单的一种就像我们上面写的,对象没有本地声明名字,只返回一个对象这样:

var Module = (function () {

  var privateMethod = function () {};

  return {
    publicMethodOne: function () {
      // I can call `privateMethod()` you know...
    },
    publicMethodtwo: function () {

    },
    publicMethodThree: function () {

    }
  };

})();

本地域对象字面量 (Locally scoped Object Literal)

本地域的意思是一个变量/函数在一个域内声明。在 Conditionizr 项目中,大概 100 行的文件我们用了一个本地领命名空间,这有就方便看清公开和私有方法,无需检查返回状态。在这个意义上讲,这是更容易看清哪些是公开的,因为它们附带有一个本地域命名空间:

var Module = (function () {

  // locally scoped Object
  var myObject = {};

  // declared with `var`, must be "private"
  var privateMethod = function () {};

  myObject.someMethod = function () {
    // take it away Mr. Public Method
  };

  return myObject;

})();

你会看到在 Module 中最后一行返回了 myObject 我们的全局 Module 不会关心本地域对象的名字,我们只会得到传回的实际对象而不是名字。这提供了更好的代码管理。

堆叠的本地域对象字面量 (Stacked locally scoped Object Literal)

这个看起来跟上一个例子很像,但使用“传统”单对象字面量标记法:

var Module = (function () {

  var privateMethod = function () {};

  var myObject = {
    someMethod:  function () {

    },
    anotherMethod:  function () {

    }
  };

  return myObject;

})();

看过以后,我更喜欢第二个方式,本地域对象字面量。因为这里,我们必须在我们使用之前先声明其他函数(你应该这样做,因为用 function myFunction() {} 提升你的函数当使用不当的时候会引发问题)。使用 var myFunction = function() {}; 语法让你不用担心这个问题,而且我们在使用它之前做声明,这也让调试起来更简单,JavaScript 解析器会按照我们声明的顺序运行代码,比提升函数声明好很多。但我也不太喜欢这种方式,因为“堆叠的”方法通常看起来很累赘,而且没有明显的本地域对象命名空间去栓着公开方法在上面。

揭示模块模式 (Revealing Module Pattern)

我们看那模块,有一种非常干净利索的变体被称为“揭示 (revealing)”模式,我们揭示公开指针引用到模块域内部的方法。再一次,可以创造出一个非常好的代码管理系统,因为你可以看到定义的哪个方法是转向那个模块内的:

var Module = (function () {

  var privateMethod = function () {
    // private
  };

  var someMethod = function () {
    // public
  };

  var anotherMethod = function () {
    // public
  };

  return {
    someMethod: someMethod,
    anotherMethod: anotherMethod
  };

})();

我非常喜欢上面的语法,这是非常有声明性的。对于更大的 JavaScript 模块,这个模式帮助更大,使用标准的“模块模式”可以脱离你所用的语法上的依赖,及你如何组织代码。

访问“私有”方法 (Accessing “Private” Methods)

你可能在看这篇文章的过程中在想,“那么如果我写了些私有方法,我怎样调用它们?”这就是 JavaScript 变格外爽的地方,允许我们在公开方法里面真正地调用私有方法。见:

var Module = (function () {

  var privateMethod = function (message) {
    console.log(message);
  };

  var publicMethod = function (text) {
    privateMethod(text);
  };

  return {
    publicMethod: publicMethod
  };

})();

// Example of passing data into a private method
// the private method will then `console.log()` 'Hello!'
Module.publicMethod('Hello!');

但不仅仅限于方法,你可以访问 Object, Array 及其他:

var Module = (function () {

  var privateArray = [];

  var publicMethod = function (somethingOfInterest) {
    privateArray.push(somethingOfInterest);
  };

  return {
    publicMethod: publicMethod
  };

})();

扩展模块 (Augmenting Modules)

至此,我们创建了非常棒的模块,返回了一个对象。但如果我们想扩展我们的模块,包含另一个更小的模块来扩展我们原有的模块?

假设代码是这样的:

var Module = (function () {

  var privateMethod = function () {
    // private
  };

  var someMethod = function () {
    // public
  };

  var anotherMethod = function () {
    // public
  };

  return {
    someMethod: someMethod,
    anotherMethod: anotherMethod
  };

})();

我们想象一下,这是我们应用程序中的一部分,但我们的设计不打算包含一些东西到应用程序核心部分,因此我们可以讲它作为独立模块来包含,创建一个扩展。

至此我们的模块 Object 如此:

Object {someMethod: function, anotherMethod: function}

但如果我们想添加我们的模块扩展,就回以另一个公开方法结尾,可能像这样:

Object {someMethod: function, anotherMethod: function, extension: function}

现在可以用第三个方法了,但我们怎么去管理它?我们创建一个贴切的名字叫 ModuleTwo,并且传到我们的 Module 命名空间里,这就使得我们可以访问扩展的对象:

var ModuleTwo = (function (Module) {

    // access to `Module`

})(Module);

之后我们可以在模块里创建另一个方法,可以运用到私有域 / 函数式的便利,并返回我们扩展的方法。我的伪代码是这样的:

var ModuleTwo = (function (Module) {

    Module.extension = function () {
        // another method!
    };

    return Module;

})(Module || {});

Module 获得传入的 ModuleTwo,一个扩展方法添加进来然后再次返回。我们的对象变得杂乱,但这是因为 JavaScript 的灵活性 :D

然后我可以看到(通过一些如 Chrome 开发者工具)我初始的 Module 现在有了第三个属性:

// Object {someMethod: function, anotherMethod: function, extension: function}
console.log(Module);

这里还有另一个提醒,你会发现我传进第二个 ModuleTwo 的是 Module || {} 这是以防万一 Module 是 undefined ——我们现在不想引发错误 ;) 这里的做法是实例化一个新对象,并且绑上扩展方法,并返回它。

私有命名约定(Private Naming Conventions)

我个人喜欢揭示模块模式,但假如,我代码里有很多看起来声明都一样的函数,当我检阅代码的时候看起来都一样。我有时候创建一个本地域对象,但有时候不。不这么干的时候,我是怎样分清私有变量/方法呢?用 _ 字符!你可能看到网页上到处都有这个,你现在知道我们为什么这样做了:

var Module = (function () {

  var _privateMethod = function () {
    // private stuff
  };

  var publicMethod = function () {
    _privateMethod();
  };

  return {
    publicMethod: publicMethod
  };

})();