JavaScript设计模式-单例、模块

之前在《JavaScript小特性-面向对象》里面介绍过JavaScript面向对象的特性了,有了面向对象之后,自然就会想——那是不是还有设计模式呢?由于js面向对象的方式是基于原型(prototype)的,而不是传统基于类型(class),所以js的设计模式也和经典的设计模式有些差异。

 

关于设计模式

先说说什么是设计模式吧。很多人都觉得“设计模式”这东西很玄乎,把Gof四人帮的《Design Patterns》奉为编程圣经,而我却觉得大可不必。设计模式说白了,就是在特定环境下解决某类常见问题的一种套路,按着这种套路去做就会很得心应手。但是,这并不代表这些设计模式是放之四海皆准,更不是说套着设计模式做的东西就一定是最好维护、性能最佳、至高无上的了。就算你不知道什么设计模式,只要思路正确,也能写出设计模式来。

有大师说,设计模式其实是对语言缺陷的一种弥补。这里有两层意思,一是说设计模式是基于语言的,因为语言存在缺陷,能力有所不足,所以需要设计模式去提供一种通用的解决方式去弥补这些不足;二是说,如果脱离具体语言及具体环境,生搬硬套设计模式甚至会是一种束缚,设计模式并非绝对正确。

了解了上面的观点之后,下面就通过js的单例模式来看看,JavaScript的设计模式是怎么回事。

 

单例模式(Singleton)

传统的单例模式大概是这么回事(Java版):

public class Singleton{
    private static Singleton instance;
    private int property;//某些属性
    private Singleton(){
        //通过私有的构造函数来保证只有一个实例
    }
    public static synchronized Singleton getInstance(){
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
    public void method(){//某些方法}
}

传统单例模式的特点就是利用私有构造函数来保证只有一个实例,使不同地方调用的都是同一个实例对象。然而这一点对于js来说就有点弱爆了,因为js是允许全局变量的,解决单例的办法很简单,定义一个全局对象就完事了(是不是觉得很原始,完全没什么模式可言):

var jsSingleton = {
    property:"something",
    method:function(){
        console.log('hello world');
    }
}

调用也很简单,无需顾及什么类名,任何地方访问jsSingleton都是同一个对象,完全不用担心。如果你气急败坏的跟我说,你这只是饿汉式的,懒汉式单例呢?如果真的创建对象时很耗费资源,或者是你对性能洁癖到一定程度,那也只能让js东施效颦的学一下传统的方式了:

var jsLazySingleton =(function(){
    var instance;
    //利用闭包来解决私有构造函数的问题(后面有解释)
    function init (){
        return {
            property: 'some thing',
            method: function(){}
        }
    }
    return {
        getInstance :function(){
            //如果instance存在则返回,不存在则调用init()
            return instance || instance = init();
        }
    }
})();

 

模块模式(Module)

在js的懒汉式单例模式中,其实也用到了另一种设计模式,即模块模式。在传统软件工程中,模块模式被定义为给类提供私有和公共封装的一种方法,也就是我们常说的“模块化”。在Java里面Class就是一种模块,解决了属性、方法的封装问题,它的模块模式直接就融合到语言特性里面了,因此也没什么模块模式的说法;然而对于JavaScript这种过于灵活的语言,这种最基本的私有化封装却需要用一个设计模式来解决了。

在JavaScript中,“private”是作为保留字,而不是关键字的,也就是说,JavaScript没有私有化这一功能(纠结吧)。解决解决这问题有两种方法,一是定义变量的时候在前面加上下划线“_”,也就是告诉其他开发者,不要动这个变量哟;另一种则是利用闭包。第一种方法并不是真正的私有,只是一种规范,如果要做到真正的私有,还是要用第二种方法——闭包。

我们创建一个匿名函数,然后立即运行它,此匿名函数中的所有代码都存在于一个“闭包”之中,从而得到私有性,并在特定作用域中保持可被访问。方法如下:

(function () {
    //此作用域的所有变量、函数依旧可在特定作用域中被访问
})();

先用括号把函数定义括起来,从而得到该函数对象,然后后面的括号是立即运行它。这种形式可以在很多js库中见到,例如jQuery:

(function( window, undefined ) {
    ......
    //最后一行
    window.jQuery = window.$ = jQuery;
})(window);

我们看到jQuery把window这个全局变量传进匿名函数中,然后把内部定义的jQuery赋值给了window,从而在全局作用域中都可以通过“$”符号来访问匿名函数中的内容(想了解更多关于闭包的内容,可以看看《jQuery自运行匿名函数》《闭包与作用域链》)。

弄清楚这些之后,我们就来看看js模块模式的基本样子吧:

var Module = (function () {
    var my={},
        privateVar = 8;//私有属性
    function privateFun() {//私有方法
        return ++privateVar;
    };
    my.publicVar = 1;//公共属性
    my.moduleFun = function () {//公共方法
        return privateFun();
    };
    return my;
}());
console.log(Module.publicVar);//1
console.log(Module.publicFun());//9

在匿名函数中我们返回了一个my变量给Module作为外部访问闭包内容的接口,除闭包内my之外的内容都得到了私有性保护,闭包的数据在Module变量的作用域中保持可以访问。

好了,模块模式解决了JavaScript私有化的问题,我们可以利用它来定义命名空间、单例、拥有私有化封装的对象等等。然而模块模式也并非尽善尽美。例如,我们定义私有、公共变量的方法是不同的,当开发过程中我们需要改变某个变量的可见性的时候,就不得不在它所有出现过的地方进行修改;并且JavaScript作为动态编译的语言,我们可以随时给对象添加属性、方法,然而我们在闭包之外定义的方法是无法直接访问私有数据的

 

更高级的模块模式

虽然前面提到的模块模式对于大多数开发者来说已经足够了,但是模块模式还可以被改进的更加强大、更加易于扩展。

扩展性

之前所说的模板模式有一个限制,就是整个模板必须定义在一个文件中。曾面对一大堆代码工作的人肯定明白将它划分为多个文件的意义。好在,有个巧妙的方法来扩展我们的Modules。首先,在匿名函数的参数中导入Module,然后给它添加属性,然后再导出它。下面这个例子是在另一个文件中对上面提到的Module进行扩展(必须是全局作用域的情况):

var Module = (function (my) {
    my.anotherFun = function () {
        // do someting...
    };
    return my;
}(Module||{}));//注意,这里还有个“{}”

我们重新用var关键字定义Module以保证一致性,并且如果之前Module没有被创建的话,在这里会自动的被创建为空对象“{}”。当这段代码运行结束之后,我们的Module又有了一个新的公共方法Module.anotherFun()。每个此扩展的Module都有自己独有的私有属性、方法。由于导入的时候可以自动创建,这些包含Module定义的文件之间可以以任意顺序加载(如果存在依赖关系,必须按次序加载的话,那你按次序就好了)。

跨文件私有属性共享

目前的多文件扩展Module还有个一严重的限制,那就是每个文件的Module都只保持自己的私有状态,无法访问其他文件的私有属性。当然,这也可以被解决。下面是解决多文件私有属性共享的一个方法:

var MODULE = (function () {
    //将所有的私有属性、方法都定义在_private对象中
    //每个扩展Module都可以通过my._private来访问
    var my = {},
    _private = my._private = {},

    _seal = function (){
        //密封,删除所有私有数据的可访问性
        delete my._private;
    },
    _unseal = function (){
        //解封,让私有数据重新可访问
        my._private = _private;
    };

    my.extend = function(otherModules){
        //必须通过此方法来添加扩展Module文件
        _unseal();
        //add other modules
        _seal();//异步调用,此处只是示意,真正的代码并非如此
    }
    return my;
}());

上面的Module文件必须第一个被加载,然后利用Module.extend来加载其他的扩展。my.extend实际上是通过类似Labjs的工具来并行加载其他的Module。每个扩展Module都通过my._private来传递私有性,并且都需要将传来的my._private保存为自己的私有属性,所有对my._private的修改都将反应到其他扩展Module中去。此方法参考于Ben Cherry的《JavaScript Module Pattern: In-Depth》。

 

小结一下

看完了JavaScript的单例模式和模块模式,是不是有点感叹与JavaScript设计模式的不伦不类呢?这些就是语言特性的体现,设计模式是依赖于语言特性的,是对语言能力的一种补充。JavaScript的设计模式并非不伦不类,很多JavaScript也有很多优雅的设计模式,可以让传统设计模式相形见绌的。

接下来还打算写几篇关于JavaScript设计模式的文章,敬请期待。

 

发布者

Rolf

伪文艺IT攻城师,热爱前端,热爱互联。

《JavaScript设计模式-单例、模块》有2个想法

  1. 请问 跨文件共享私有属性那段代码中 _seal和_unseal 如何被调用 以及执行的?
    _seal和_unseal 都是在外部访问不到啊?
    每太看懂这段代码啊 [泪]

发表评论

电子邮件地址不会被公开。 必填项已用*标注

您可以使用这些HTML标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

[喜欢] [嘻嘻] [奋斗] [问号] [鼓掌] [泪] [酷] [强] [耶] [握手] [心] [给力] [神马] [围观] [奥特曼] more »