javascript设计模式之单例模式

基本结构

简单的单体结构实际上就是一个对象字面量:

1
2
3
4
5
6
7
8
9
10
11
var Singleton = {
attribute1: true,
attribute2: 1,

method1: function() {
...
},
method2: function() {
...
}
}

单体结构应该是一个只能被实例化一次,并且可以通过一个访问点访问的类。所谓访问点,可以理解为一个变量,这个变量在全局范围内可以访问到,并且只有一个。

单体结构的作用

命名空间

通过命名空间将相似的方法组合到一起也可以增加代码的文档性。另一方面,网页上的Javascript代码会根据其用途有不同的划分,分不同的人来维护。例如JS库代码,广告代码等。为了避免彼此之间产生冲突,在全局对象中也可以给不同用途的代码划分各自的命名空间,也就是存到各个单体中。

1
2
3
4
5
6
7
8
9
var Classicemi = {};

Classicemi.Common = {
...
};

Classicemi.ErrorCodes = {
...
};

网页专用代码包装器

在一个网站中,有些Javascript代码是整个网站都要用到的,比如框架,库等。而有些代码是特定的网页才会用到,例如对一个页面中的DOM元素添加事件监听等。一般我们会通过给页面的load事件创建一个init方法来对所有需要的操作进行初始化,将所有的初始化代码放在一个方法中。
比如含有一个表单的页面,我们要取消submit的默认行为并添加ajax交互。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Classicemi.RegPage = {
FORM_ID: 'reg-form',
OUTPUT_ID: 'reg-results',

// 表单处理方法
handleSubmit: function(e) {
e.preventDefult();
...
} ,
sendRegistration: function(data) {
... // 发送XHR请求
},
...

// 初始化方法
init: function() {
Classicemi.RegPage.formEl = $(Classicemi.RegPage.FORM_ID);
Classicemi.RegPage.outputEl = $(Classicemi.RegPage.OUTPUT_ID);

addEvent(Classicemi.RegPage.FormEl, 'submit', Classicemi.RegPage.handleSubmit); // 添加事件
}
};
// 页面加载后运行初始化方法
addLoadEvent(Classicemi.PageName.init);

在单体中表示私用成员

对象中有时候有些属性和方法是需要进行保护,避免被修改的,这些成员称为私用成员。在单体中声明私用成员也是保护变量的一个好方法,另外,单体中创建私用成员的另一个好处在于由于单体只会被实例化一次,定义私用成员的时候就不用过多考虑内存浪费的问题。

使用闭包

第一步,我们通过一个构造函数返回一个空对象,这就是单体对象的初始化:

var Classicemi.Singleton = (function() {
  return {};
})();

我们通过一个自执行构造函数返回单体对象的实例,下面就可以在这个构造函数中添加我们需要的私用对象了。

var Classicemi.Singleton = (function() {
  // 私用属性
  var privateAttribute = true;

  // 私用方法
  function privateMethod() {
    ...
  }
  return {};
})();

可以公开访问的公开属性和方法可以写在构造函数返回的对象中:

var Classicemi.Singleton = (function() {
  // 私用属性
  var privateAttribute = true;

  // 私用方法
  function privateMethod() {
    ...
  }
  return {
    publicAttribute: false,

    publicMethod: function() {
      ...  
    }
  };
})();

这就是用闭包创建私有成员的方法,这种单体模式又被成为模块模式(Module Pattern),我们创建的单体可以作为模块,对代码进行组织,并划分命名空间。最大的优点就是可以创建真正的私用成员,使其不会在构造函数之外被随意修改。同时,由于单体只会被实例化一次,不用担心内存浪费的问题。

惰性实例化单体

单体一般会在页面加载过程中进行实例化,如果单体的体积比较大的话,可能会对加载速度造成影响。对于体积比较大,在页面加载时也暂时不会起作用的单体,我们可以通过惰性加载(lazy loading)的方式进行实例化,也就是在需要的时候再进行实例化。

实现惰性加载,我们要把原单体构造函数中的所有成员转移到一个内部的新构造函数中去:

Classicemi.Singleton = (function() {
  function constructor() {
    // 私用属性
    var privateAttribute = true;

    // 私用方法
    function privateMethod() {
      ...
    }
    return {
      publicAttribute: false,

      publicMethod: function() {
        ...  
      }
    };
  }
})();

在getInstance()方法内部,首先要对单体是否已经实例化进行检查,如果已经实例化过,就将其返回。如果没有实例化,就调用constructor方法。我们需要一个变量来保存实例化后的单体。

Classicemi.Singleton = (function() {
  var uniqueInstance; // 保存实例化后的单体

  function constructor() {
    ...
  }

  return {
    getInstance: function() {
      if (!uniqueInstance) {
        uniqueInstance = constructor();
      }
      return uniqueInstance;
    }
  }
})();

单体的构造函数像这样被改写后,调用其方法的代码就要由这样:

Classicemi.Singleton.getInstance().publicMethod();

分支

分支(branching)技术的意义在于根据不同的条件,对单体进行不同的实例化过程。
在构造函数中存在不同的实例对象,针对condition判断条件的不同返回值,构造函数返回不同的对象作为单体的实例。例如对不同的浏览器来说,支持的XHR对象不一样,大多数浏览器中是XMLHttpRequest的实例,早期的IE浏览器中是某种ActiveX的实例。我们在创建XHR对象的时候,可以根据不同浏览器的支持情况返回不同的实例,like this:

var XHRFactory = (function() {
  var standard = {
    createXHR: function() {
      return new XMLHttpRequest();
    }
  };
  var activeX = {
    createXHR: function() {
      return new ActiveXObject('Msxml2.XMLHTTP');
    }
  };
  var activeOld = {
    createXHR: function() {
      return new ActiveXObject('Microsofe.XMLHTTP');
    }
  }

  var testObj;
  try {
    testObj = standard.createXHR();
    return standard;
  } catch (e) {
    try {
      testObj = activeX.createXHR();
      return standard;
    } catch (e) {
      try {
        testObj = activeOld.createXHR();
        return standard;
      } catch (e) {
        throw new Error('No XHR object found in this environment.');
      }
    }
  }
})();

通过try-catch语句对浏览器XHR的支持性进行测试同时防止抛出错误,这样不同浏览器都能创建出自己支持的XHR对象的实例。

单体模式之利弊

单体模式之利

  1. 单体模式能很好的组织代码,由于单体对象只会实例化一次,单体对象中的代码可以方便地进行维护。
  2. 单体模式可以生成自己的命名空间,防止自己的代码被别人随意修改。
  3. 惰性实例化,有助于性能的提升。
  4. 分支,针对特定环境定制专属的方法。

单体模式之弊

类之间的耦合性可能增强,因为要通过命名空间去对一些方法进行访问,强耦合的后果会不利于单元测试。

文章参考:http://hao.jser.com/archive/4199/