Skip to content

JavaScript 中的单例模式

前面提到的几种单例模式的实现,更多的是接近传统面向对象语言中的实现,单例对象从“类”中创 建而来。在以类为中心的语言中,这是很自然的做法。比如在 Java 中,如果需要某个对象,就必 须先定义一个类,对象总是从类中创建而来的。但 JavaScript 其实是一门无类(class-free) 语言,也正因为如此,生搬单例模式的概念并无意义。在 JavaScript 中创建对象的方法非常简单, 既然我们只需要一个“唯一”的对象,为什么要为它先创建一个“类”呢?这无异于穿棉衣洗澡,传统 的单例模式实现在 JavaScript 中并不适用。单例模式的核心是确保只有一个实例,并提供全局访 问。

全局变量不是单例模式,但在 JavaScript 开发中,我们经常会把全局变量当成单例来使用。

javascript
var a = {}

对象 a 确实是独一无二的。如果 a 变量被声明在全局作用域下,则我们可以在代码中的任何位置 使用这个变量,全局变量提供给全局访问是理所当然的。这样就满足了单例模式的两个条件。 但是全局变量存在很多问题,它很容易造成命名空间污染。在大中型项目中,如果不加以限制和 管理,程序中可能存在很多这样的变量。JavaScript 中的变量也很容易被不小心覆盖,就像上面的 对象 var a = {};,随时有可能被别人覆盖。Douglas Crockford 多次把全局变量称为 JavaScript 中最糟糕的特性。在对 JavaScript 的创造者 Brendan Eich 的访谈中, Brendan Eich 本人也承认全局变量是设计上的失误,是在没有足够的时间思考一些东西的情况下 导致的结果。作为普通的开发者,我们有必要尽量减少全局变量的使用,即使需要,也要把它的污染 降到最低。以下几种方式可以相对降低全局变量带来的命名污染。

  • 使用命名空间

    适当使用命名空间,并不会杜绝全局变量,但可以减少全局变量的数量。

    javascript
    var namespace1 = {
      foo: function () {},
      bar: function () {}
    }

    动态创建命名空间

    javascript
    var myApp = {}
    
    myApp.namespace = function (name) {
      var parts = name.split('.')
      var current = this
    
      for (var i = 0, l = parts.length; i < l; i++) {
        var part = parts[i]
    
        if (!current[part]) {
          current[part] = {}
        }
    
        current = current[part]
      }
    }
    
    myApp.namespace('foo')
    myApp.namespace('bar.baz')
    
    // 上述代码等价于
    var myApp = {
      foo: {},
      bar: {
        baz: {}
      }
    }
  • 使用闭包封装私有变量

    这种方法把一些变量封装在闭包的内部,只暴露一些接口跟外界通信

    javascript
    var user = (function () {
      var _firstName = 'foo',
          _lastName = 'bar'
    
      return {
        getUserInfo: function () {
          return _firstName + '-' + _lastName
        }
      }
    })()

    外部访问不到 _firstName 和 _lastName 这两个变量,避免了全局变量的污染。