《JavaScript设计模式》——9.2 Module(模块)模式-阿里云开发者社区

开发者社区> 异步社区> 正文

《JavaScript设计模式》——9.2 Module(模块)模式

简介:
+关注继续查看

本节书摘来自异步社区《JavaScript设计模式》一书中的第9章,第9.2节, 作者: 【美】Addy Osmani 译者: 徐涛 更多章节内容可以访问云栖社区“异步社区”公众号查看。

9.2 Module(模块)模式

模块是任何强大应用程序架构中不可或缺的一部分,它通常能够帮助我们清晰地分离和组织项目中的代码单元。

在JavaScript中,有几种用于实现模块的方法,包括:

对象字面量表示法
Module模式
AMD模块
CommonJS模块
ECMAScript Harmony模块
我们稍后将在本书第11章探索后三种方法。Module模式在某种程度上是基于对象字面量,因此首先重新认识对象字面量是有意义的。

9.2.1 对象字面量
在对象字面量表示法中,一个对象被描述为一组包含在大括号({})中、以逗号分隔的name/value对。对象内的名称可以是字符串或标识符,后面跟着一个冒号。对象中最后的一个name/value对的后面不用加逗号,如果加逗号将会导致出错。

var myObjectLiteral = {
     variableKey: variableValue,
     functionKey: function () {
      
     }
};

对象字面量不需要使用new运算符进行实例化,但不能用在一个语句的开头,因为开始的可能被解读为一个块的开始。在对象的外部,新成员可以使用如下赋值语句添加到对象字面量上,如:myModule.property = "someValue";

下面我们可以看到一个更完整的示例:使用对象字面量表示法定义的模块:

var myModule = {
myProperty: "someValue",
// 对象字面量可以包含属性和方法
// 例如,可以声明模块的配置对象
myConfig: {
   useCaching: true,
   language: "en"
},
// 基本方法
myMethod: function () {
  console.log("Where in the world is Paul Irish today?");
},
// 根据当前配置输出信息
myMethod2: function () {
  console.log("Caching is:" + (this.myConfig.useCaching) ? "enabled" : "disabled");
},
// 重写当前的配置
myMethod3: function (newConfig) {
   if (typeof newConfig === "object") {
       this.myConfig = newConfig;
       console.log(this.myConfig.language);
   }
 }
};
// 输出:Where in the world is Paul Irish today?
myModule.myMethod();
//输出:enabled
myModule.myMethod2();
//输出:fr
myModule.myMethod3({
   language: "fr",
   useCaching: false
});

使用对象字面量有助于封装和组织代码,如果想进一步了解有关对象字面量的信息,丽贝卡·墨菲曾对这一主题进行了深入解析,可阅读其文章进行了解。

也就是说,如果我们选择了这种技术,我们可能同样也对Module模式感兴趣。它仍然使用对象字面量,但只是作为一个作用域函数的返回值。

9.2.2 Module(模块)模式
Module模式最初被定义为一种在传统软件工程中为类提供私有和公有封装的方法。

在JavaScript中,Module模式用于进一步模拟类的概念,通过这种方式,能够使一个单独的对象拥有公有/私有方法和变量,从而屏蔽来自全局作用域的特殊部分。产生的结果是:函数名与在页面上其他脚本定义的函数冲突的可能性降低(见图9-2)。
screenshot

9.2.2.1 私有
Module模式使用闭包封装“私有”状态和组织。它提供了一种包装混合公有/私有方法和变量的方式,防止其泄露至全局作用域,并与别的开发人员的接口发生冲突。通过该模式,只需返回一个公有API,而其他的一切则都维持在私有闭包里。

这为我们提供了一个屏蔽处理底层事件逻辑的整洁解决方案,同时只暴露一个接口供应用程序的其他部分使用。该模式除了返回一个对象而不是一个函数之外,非常类似于一个立即调用的函数表达式1。

应该指出的是,在JavaScript中没有真正意义上的“私有”,因为不像有些传统语言,JavaScript没有访问修饰符。从技术上来说,我们不能称变量为公有或是私有,因此我们需使用函数作用域来模拟这个概念。在Module模式内,由于闭包的存在,声明的变量和方法只在该模式内部可用。但在返回对象上定义的变量和方法,则对外部使用者都是可用的。

9.2.2.2 历史
从历史的角度来看,Module模式最初是在2003年由多人共同开发出来的,其中包括理查德•康佛德。后来由道格拉斯·克劳克福德在其讲座中推广开来。除此之外,如果你曾体验过雅虎的YUI库,它的一些特性看起来可能相当熟悉,原因是在创建它们的组件时,Module模式对YUI有很大的影响。

9.2.2.3 示例
让我们通过创建一个自包含的模块来看一下Module模式的实现。

var testModule = (function () {
   var counter = 0;
   return {
     incrementCounter: function () {
        return ++counter;
     },
     resetCounter: function () {
       console.log("counter value prior to reset: " + counter);
       counter = 0;
       }
   };
})();

//用法:
//增加计数器

testModule.incrementCounter();

// 检查计数器值并重置
//输出:1

testModule.resetCounter();

在这里,代码的其他部分无法直接读取incrementCounter()或resetCounter()。counter变量实际上是完全与全局作用域隔离的,因此它表现得就像是一个私有变量,它的存在被局限于模块的闭包内,因此唯一能够访问其作用域的代码就是这两个函数。上述方法进行了有效的命名空间设置,所以在测试代码中,所有的调用都需要加上前缀(如:“testModule”)。

使用Module模式时,可能会觉得它可以用来定义一个简单的模板来入门使用。下面是一个包含命名空间、公有和私有变量的Module模式:

var myNamespace = (function () {
// 私有计数器变量
var myPrivateVar = 0;
// 记录所有参数的私有函数
var myPrivateMethod = function (foo) {
     console.log(foo);
    };
return {
   // 公有变量
   myPublicVar: "foo",
   // 调用私有变量和方法的公有函数
   myPublicFunction: function (bar) {
      // 增加私有计数器值
      myPrivateVar++;
     // 传入bar调用私有方法
     myPrivateMethod(bar);
   }
};
})();

来看另一个示例,我们可以看到一个使用这种模式实现的购物车。模块本身是完全自包含在一个被称为basketModule的全局变量中。模块中的basket数组是私有的,因此应用程序的其他部分无法直接读取它。它只与模块的闭包一起存在,所以能够访问它的方法都是那些能够访问其作用域的方法(即addItem()、getItem()等)。

var basketModule = (function () {
   // 私有
   var basket = [];
   function doSomethingPrivate() {
    //...
   }
   function doSomethingElsePrivate() {
    //...
   }
   // 返回一个暴露出的公有对象
   return {
      // 添加item到购物车
      addItem: function (values) {
        basket.push(values);
      },
     // 获取购物车里的item数
     getItemCount: function () {
       return basket.length;
      },
      // 私有函数的公有形式别名
      doSomething: doSomethingPrivate,
      // 获取购物车里所有item的价格总值
      getTotal: function () {
        var itemCount = this.getItemCount(),
            total = 0;
        while (itemCount--) {
          total += basket[itemCount].price;
        }
        return total;
     }
   };
})();

在该模块中,可能已经注意到返回了一个object。它会被自动赋值给basketModule,以便我们可以与它交互,如下所示:

// basketModule返回了一个拥有公用API的对象
basketModule.addItem({
   item: "bread",
   price: 0.5
});
basketModule.addItem({
   item: "butter",
   price: 0.3
});

// 输出: 2

console.log(basketModule.getItemCount());

// 输出: 0.8

console.log(basketModule.getTotal());

// 不过,下面的代码不会正常工作
// 输出:undefined
// 因为basket自身没有暴露在公有的API里

console.log(basketModule.basket);

// 下面的代码也不会正常工作,因为basket只存在于basketModule闭包的作用域里,而不是存在于返回的公有对象里

console.log(basket);

上述方法在basketModule内部都属于有效的命名空间设置。

请注意上面的basket模块中的作用域函数是如何包裹在所有函数的周围,然后调用并立即存储返回值。这有很多优点,包括:

只有我们的模块才能享有拥有私有函数的自由。因为它们不会暴露于页面的其余部分(只会暴露于我们输出的API),我们认为它们是真正的私有。
鉴于函数往往已声明并命名,在试图找到有哪些函数抛出异常时,这将使得在调试器中显示调用堆栈变得更容易。
正如 T.J.Crowder 在过去所指出的,根据环境,它还可以让我们返回不同的函数。在过去,我曾看到开发人员使用它来执行UA测试,从而针对IE在他们的模块内提供一个代码路径,但我们现在可以很容易地选择特征检测来实现类似的目的。
9.2.3 Module模式变化
9.2.3.1 引入混入
模式的这种变化演示了全局变量(如:jQuery、Underscore)如何作为参数传递给模块的匿名函数。这允许我们引入它们,并按照我们所希望的为它们取个本地别名。

// 全局模块
var myModule = (function (jQ, _) {
     function privateMethod1() {
          jQ(".container").html("test");
     }
     function privateMethod2() {
       console.log(_.min([10, 5, 100, 2, 1000]));
     }
     return {
          publicMethod: function () {
               privateMethod1();
          }
     };
// 引入jQuery和Underscore
})(jQuery, _));
myModule.publicMethod();

9.2.3.2 引出
下一个变化允许我们声明全局变量,而不需实现它们,并可以同样地支持上一个示例中的全局引入的概念。

// 全局模块
var myModule = (function (){
      // 模块对象
var module = {},
   privateVariable = "Hello World";
function privateMethod() {
  // ...
}
module.publicProperty = "Foobar";
module.publicMethod = function () {
   console.log(privateVariable);
};
return module;
})();

9.2.3.3 工具包和特定框架的Module模式实现
Dojo。提供了一种和对象一起用的便利方法dojo.setObject()。其第一个参数是用点号分割的字符串,如myObj.parent.child,它在parent对象中引用一个称为child的属性,parent对象是在myObj内部定义。我们可以使用setObject()设置子级的值(比如属性等),如果中间对象不存在的话,也可以通过点号分割将中间的字符作为中间对象进行创建。

例如,如果要将basket.core声明为store名称空间的对象,可以采用传统的方法来实现,如下所示:

var store = window.store || {};
if (!store["basket"]) {
   store.basket = {};
}
if (!store.basket["core"]) {
   store.basket.core = {};
}
store.basket.core = {
   // ...剩余的逻辑
};

或者,使用Dojo 1.7(AMD兼容的版本)和上述方法,如下所示:

require(["dojo/_base/customStore"], function (store) {
  // 使用 dojo.setObject()
  store.setObject("basket.core", (function () {
       var basket = [];
       function privateMethod() {
            console.log(basket);
       }
       return {
            publicMethod: function (){
                      privateMethod();
            }
       };
  })());
});

ExtJS。对比那些使用Sencha ExtJS的人,你的运气会好一点,因为官方文档包含了一些示例,演示了EXTJS框架下如何正确使用Module模式。

在这里,我们可以看到这样的一个示例:如何定义一个名称空间,然后填充一个包含私有和公有 API 的模块。除了一些语义差异,它与如何在纯JavaScript中实现Module模式十分相近。

// 创建命名空间
Ext.namespace("myNameSpace");
// 创建应用程序
myNameSpace.app = function () {
  // 这里不要访问DOM,因为元素还不存在
  // 私有变量
 var btn1,
      privVar1 = 11;
// 私有函数
var btn1Handler = function (button, event) {
     console.log("privVar1", privVar1);
     console.log("this.btn1Text=" + this.btn1Text);
  };
// 公有对象
return {
  // 公有属性,例如要转化的字符

btn1Text: "Button 1",

  // 公有方法
init: function () {
    if (Ext.Ext2) {
      btn1 = new Ext.Button({
         renderTo: "btn1-ct",
         text: this.btn1Text,
         handler: btn1Handler
      });
    } else {
      btn1 = new Ext.Button("btn1-ct", {
        text: this.btn1Text,
        handler: btn1Handler
      });
    }
 }
};
}();

YUI。同样,在使用 YUI3 构建应用程序时,我们也可以实现Module模式。下面的示例在很大程度上基于由Eric Miraglia提出的原始YUI Module模式实现,但它又与纯JavaScript版本截然不同。

Y.namespace("store.basket") = (function () {
     var myPrivateVar, myPrivateMethod;
     // 私有变量:
     myPrivateVar = "I can be accessed only within Y.store.basket.";
     // 私有方法:
     myPrivateMethod = function () {
          Y.log("I can be accessed only from within YAHOO.store.basket");
     }
     return {
         myPublicProperty: "I'm a public property.",
         myPublicMethod: function () {
              Y.log("I'm a public method.");
             // 在basket里,可以访问到私有变量和方法
             Y.log(myPrivateVar);
             Y.log(myPrivateMethod());
            // myPublicMethod的原始作用域是store,所以可以使用this来访问公有成员
            Y.log(this.myPublicProperty);
      }
   };
})();

jQuery。有许多方式可以将非jQuery插件代码包装在Module模式中。如果模块之间有多个共性,Ben Cherry之前建议过一种实现,在模块模式内部模块定义附件使用函数包装器。

在下面的示例中,定义了library函数,它声明一个新库,并在创建新库(即模块)时将init函数自动绑定到document.ready。

function library(module) {
   $(function () {
    if (module.init) {
      module.init();
    }
   });
   return module;
}
var myLibrary = library(function () {
   return {
     init: function () {
       // module implementation
// 模块实现
     }
  };
})();

9.2.3.4 优点
我们已经了解了单例模式如何有用,但为什么Module模式是一个好的选择呢?首先,相比真正封装的思想,它对于很多拥有面向对象背景的开发人员来说更加整洁,至少是从JavaScript的角度。

其次,它支持私有数据,因此,在Module模式中,代码的公有(public)部分能够接触私有部分,然而外界无法接触类的私有部分。

9.2.3.5 缺点
Module模式的缺点是:由于我们访问公有和私有成员的方式不同,当我们想改变可见性时,实际上我们必须要修改每一个曾经使用过该成员的地方。

我们也无法访问那些之后在方法里添加的私有成员。也就是说,在很多情况下,如果正确使用,Module模式仍然是相当有用的,肯定可以改进应用程序的结构。

其他缺点包括:无法为私有成员创建自动化单元测试,bug需要修正补丁时会增加额外的复杂性。为私有方法打补丁是不可能的。相反,我们必须覆盖所有与有bug的私有方法进行交互的公有方法。另外开发人员也无法轻易地扩展私有方法,所以要记住,私有方法并不像它们最初显现出来的那么灵活。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
《JavaScript设计模式》——1.8 方法还可以这样用
没错,但是你发现没,你调用了3个方法,但是你对对象a书写了3遍。这是可以避免的,那就要在你声明的每一个方法末尾处将当前对象返回,在JavaScript中this指向的就是当前对象,所以你可以将它返回。例如我们开始写的第一个对象还记得么?改动它很简单,像下面这样就可以。
986 0
阿里云服务器端口号设置
阿里云服务器初级使用者可能面临的问题之一. 使用tomcat或者其他服务器软件设置端口号后,比如 一些不是默认的, mysql的 3306, mssql的1433,有时候打不开网页, 原因是没有在ecs安全组去设置这个端口号. 解决: 点击ecs下网络和安全下的安全组 在弹出的安全组中,如果没有就新建安全组,然后点击配置规则 最后如上图点击添加...或快速创建.   have fun!  将编程看作是一门艺术,而不单单是个技术。
4503 0
使用OpenApi弹性释放和设置云服务器ECS释放
云服务器ECS的一个重要特性就是按需创建资源。您可以在业务高峰期按需弹性的自定义规则进行资源创建,在完成业务计算的时候释放资源。本篇将提供几个Tips帮助您更加容易和自动化的完成云服务器的释放和弹性设置。
7768 0
《JavaScript设计模式》——2.2 包装明星——封装
闭包是有权访问另外一个函数作用域中变量的函数,即在一个函数内部创建另外一个函数。我们将这个闭包作为创建对象的构造函数,这样它既是闭包又是可实例对象的函数,即可访问到类函数作用域中的变量,如bookNum这个变量,此时这个变量叫静态私有变量,并且checkBook()可称之为静态私有方法。
1141 0
javascript设计模式--封装和信息隐藏(上)
  今天博文关注的是javascript中的封装,文章内容来自《pro javascript design patterns》(有兴趣的朋友可以直接去下)和自己对这一问题的理解。   本文分上下两部分,上部讲基本模式(basic patterns):完全暴露法,下划线标记法和使用闭包;下部讲高级模式(Advanced Patterns),如何实现静态方法和属性,常量还有其他一些知识点。
769 0
javascript设计模式--继承(下)
  本章的主题是继承,在javascript中要实现继承比其他面相对象语言要复杂的多,他主要使用原型实现继承。下面就介绍几种常用的实现继承的方式。   1.经典继承(Classical Inheritance)   我们首先创建一个Person类。
586 0
《JavaScript设计模式》——第2章 写的都是看到的——面向对象编程 2.1两种编程风格——面向过程与面向对象
面向对象编程就是将你的需求抽象成一个对象,然后针对这个对象分析其特征(属性)与动作(方法)。这个对象我们称之为类。面向对象编程思想其中有一个特点就是封装,就是说把你需要的功能放在一个对象里。比如你大学毕业你来公司携带的行李物品没有一件一件拿过来,而是要将他们放在一个旅行箱里,这样不论携带还是管理都会更方便一些。
1229 0
+关注
异步社区
异步社区(www.epubit.com)是人民邮电出版社旗下IT专业图书旗舰社区,也是国内领先的IT专业图书社区,致力于优质学习内容的出版和分享,实现了纸书电子书的同步上架,于2015年8月上线运营。公众号【异步图书】,每日赠送异步新书。
11940
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
文娱运维技术
立即下载
《SaaS模式云原生数据仓库应用场景实践》
立即下载
《看见新力量:二》电子书
立即下载