Skip to content
jnotnull edited this page Jul 17, 2014 · 9 revisions

原文链接:http://toddmotto.com/mastering-the-module-pattern/ 翻译:jnotnull


我是一个JS模块模式的超级fan,因此我很乐于和大家分享一些使用场景、这些场景中有哪些不同,以及为什么它们如此重要。模块模式就是我们常说的一种设计模式,非常有用。它非常吸引我,因为它使得作用域变得简单,同时也不会使得涉及过于复杂。

同时它使得代码变得非常简单,便于阅读和使用,巧妙的使用了对象,也不像使用prototype那样膨胀你的代码。我想我会针对模块中最棒的部分分享我的想法,如何掌握它以及它的变量和属性。

创建一个模块

为了弄明白模块能给你带来什么,你首先得明白如下函数做了什么:

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

这段代码定义了一个立即执行的函数,这就是著名的IIFE,函数创建了一个新的私有作用域。JS中并没有私有的概念,但是可以创建了一个新的作用域用来包含我们的函数逻辑在里面。所以我们可以只包含我们想要的部分到这个作用域中,其他的放到global中。

创建了新的作用域后,我们需要给我们的代码一个命名空间,这样我们就可以访问里面return出来的方法了。下面让我们来为我们的匿名模块创建一个命名空间。

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

我们在global中定义了一个模块Module,我们可以在任何地方可以访问它,甚至可以把它传递给其它模块。

私有方法

你可能已经提说过JS中的私有方法,但是严格意义上说是没有的,不过我们可以创建它。

关于私有方法你会关注它什么呢?作为私有方法我们是不希望用户能够在外部访问到它的。我们可能是用来做服务端访问或者传递敏感数据的,我们不希望把这些方法设为公有,他们可能发送hack,那我们的代码就不完全了。因此我们可以创建闭包来保护我们的代码。这也不仅仅是为了安全,同时也是为了防止命名冲突。我打赌,在你第一次写jQuery、JS代码的时候,你把所有的代码卸载了一个文件中,里面充斥着大量的function。没想到这些都是全局的,你可能会在默写时候会遇到冲突。如果是这样,你就知道为什么去改变它了。

现在让我们使用之前创建的Module,让外界不能够访问。对于模块模式的初学者来说,这个例子能够帮助你理解怎么定义一个私有方法:


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

})();

这个例子声明了一个内部新的作用域内的privateMethod函数,如果我们试图在外部域访问它,会得到一个错误,然后代码终止。我们不希望任何人去访问这个方法,特别是有人可能修改数据或者去访问服务器。

谈谈“return”

经典的模块会使用return得到一个Ojbect对象,这样通过Module命名空间就可以访问这个对象中绑定的方法了。

一个真实的轻量级的返回带有函数的对象的例子:

var Module = (function () {
  
  return {
    publicMethod: function () {
      // code
    }
  };

})();

因为我们返回了一个对象字面量,所以我们能够像访问对象字面量那样访问它们。

Module.publicMethod();

当然也可以不用对象字面的语法访问,一个标注的对象语法例子如下:

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

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

但是对于模式来说,我们不去关心是否用对象字面量,毕竟它能够附有私有方法在上面,我们可以把他当成正常的对象,我们能够访问到这些方法。这就是模块的由来,我们自己来定义哪些是私有的,然后只返回公有的给外部。

让我们再来看一个对象字面量语法,以及一个非常好的模块模式和返回值之前的关系。通常一个模块会返回一个对象,但是如果定义和构建这个对象是摆在我们面前的问题。根据具体场景,我会用一些语法糖。

返回匿名对象字面量

最简单的方式就是我们上面说的,对象不用声明名称,直接返回一个对象。看下这个代码:


var Module = (function () {

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

    },
    publicMethodThree: function () {

    }
  };

})();

本域对象语法

本域的意思就是一个变量或者方法申明在当前域中。在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。我们的global模块不会关心内部定义的对象是否有名称,我们只负责把真正的对象发送出去,而不是名字,这对你的编码会非常有帮助。

多个本地域对象语法

这个和上面的例子很雷同。但是使用了传统的单个对象语法:

var Module = (function () {

  var privateMethod = function () {};

  var myObject = {
    someMethod:  function () {

    },
    anotherMethod:  function () {
      
    }
  };
  
  return myObject;

})();

我比较喜欢这种本地域对象语法。因为在这里,我们必须先声明然后才能使用(你必须这么做,使用function myFunction () {}会提升你的函数,如果使用不正确会导致一些问题)。使用var myFunction = function () {};语法让我们不必担心这个,因为在使用前我们都已经定义了,这也是的调试变得非常容易,因为JS会按照我们定义的顺序解析,不会提升函数声明。我也不喜欢上面描述的这种方式,因为多个方法经常多余的巡检,同时也没有一个冥想的域对象命名空间来阻止公有方法被外部访问。

解密模块模式

我们已经仔细学习了模块,但是仍有一些东西有待揭秘。现在我们可以创建一个优秀的模块放到代码管理系统中了。从代码中你能够清晰的看到哪些模块被传递给Module了。

var Module = (function () {

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

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

  var anotherMethod = function () {
    // public
  };
  
  return {
    someMethod: someMethod,
    anotherMethod: anotherMethod
  };

})();

我非常喜欢上面的语法,因为非常直观。对于大部分JS模块,这种标准的模块模式模式可以帮助我们决定我们的代码框架。

接触私有方法

你可能在读这篇文章过程中想:“如果我定义了私有方法,我如何访问它们呢”。这就就JS强大的地方,它允许我们我们通过公有方法访问私有方法。

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!');

当然不仅仅是方法,还可以是对象,数组,任何东西:

var Module = (function () {

  var privateArray = [];

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

})();

增强模块

迄今为止我们已经创建了一个非常好的能够返回对象的模块。但是我们想扩展我们的模块我们该怎么做了,如何包含另一个小的模块,如何扩展原始模块。

我们先看下下面代码:

var Module = (function () {

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

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

  var anotherMethod = function () {
    // public
  };
  
  return {
    someMethod: someMethod,
    anotherMethod: anotherMethod
  };

})();

我们假设踏实我们系统的一部分,但是设计的时候我们决定我们不能在core部分加入任何东西,我们需要创建扩展来包含一个独立的模块。

当前我们的模块对象看起来应该这样

但是如果我想增加模块扩展该怎么做了,因此我们在结尾的地方顶一个一个公有的方法,如下代码:

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

第三方模块现在可用了,但是我们如何使用呢?让我们创建一个ModuleTwo,传递我们的模块名称,能够访问到我们的扩展对象:

var ModuleTwo = (function (Module) {
    
    // access to `Module`
    
})(Module);

我们在这个模块里面创建了一个新的方法,对于私有域或者方法访问非常便利。伪代码如下:

var ModuleTwo = (function (Module) {
    
    Module.extension = function () {
        // another method!
    };
    
    return Module;
    
})(Module || {});

Module传递到了ModuleTwo,增加了一个扩展方法后就返回了。我们的对象贯彻其中,这就是JS的可扩展性。

我的这个模块现在已经有了一个第三放属性了。

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

有个提示,你会发现我传递了Module || {} 到ModuleTwo,这是防止Module是未定义的-我们不想引起错误。这里做的就是初始化一个新的对象,然后绑定了我们的扩展方法,然后返回。

私有名称约定

我个人非常喜欢《模块模式揭秘》,到目前为止,我会对许多方法做点睛之笔,虽然看起来很像。我有时会创建一个本地域对象,但是有时候不会。如果我不创建,我如何区分出私有的变量和方法呢?请使用_字符吧。你可能在网上经常看到它,现在你知道为什么了吧:

var Module = (function () {

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

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

})();
Clone this wiki locally