JavaScript学习笔记(四十一) 单例

设计模式(Design Patterns)

Gang of Four书中提到的设计模式(design patterns)提供了关于面向对象软件设计常见问题的解决方案。它们已经存在很长时间了并且被证明在很多情况下是有用的。这也是为什么熟悉它们并讨论它们是有帮助的。

虽然这些设计模式是独立于语言的和实现无关的,它们已经被研究了很多年,主要从强类型静态类型语言的观点,比如C++和Java。

JavaScript作为一个无类型(untyped)动态基于原型(prototype-based)的语言,有时实现这些设计模式中一些模式会惊人的简单,甚至不重要(trivial)。

让我们以第一个例子单例模式(singleton pattern)——开始比较JavaScript和其他静态基于类的语言有什么不同。

单例(Singleton)

单例模式的概念是一个特殊类只有一个实例。这意味着你第二次使用相同类去创建对象,你应该得到的是第一次创建的同一个对象。
然而如何用JavaScript做到呢?在JavaScript中没有类,仅仅是对象。当你创建一个新对象,实际没有任何一个对象和它相同,并且新对象已经是一个单例。
使用对象字面量创建一个简单对象也是一个单例的例子:
var obj = {
    myprop: 'my value'
};
在JavaScript中,对象从来不相等除非它们是同一个对象,所有即使你创建一个相同对象使用明确相同的成员,它也不会和第一个相同:
var obj2 = {
    myprop: 'my value'
};
obj === obj2; // false
obj == obj2; // false
所以你可以说你每次用对象字面量创建一个对象,你已经创建了一个单例,且不需要包含任何特殊的语法。

使用new(Using new)

JavaScript没有类,那么对于单例逐字的定义没有技术意义。但JavaScript有使用构造函数创建对象new语法,并且有时候你可能想使用这种语法实现一个单例、
其思想是当你使用相同的构造函数采用new去创建几个对象时,你应该得到的仅仅是指向完全相同对象的新指针(pointers)。

下面这段代码展示了期望的行为:
var uni = new Universe();
var uni2 = new Universe();
uni === uni2; // true
在这个例子中,uni仅在第一次构造函数调用时被创建。第二次(和第三次,第四次等)相同的uni对象被返回。这也是为什么uni === uni2——因为它们本质上是两个引用指向完全相同的对象。那么如何在JavaScript实现这个呢?

你需要让你的Universe构造函数缓存对象实例this—在被创建之后并且在构造函数第二次被调用时返回它,你有几个选项去实现这个:
  • 你可以使用一个全局变量去存储这个变量。这是不被推荐的因为一般原则全局变量是不好的。另外,任何人可以重写这个全局变量,甚至是无意中。那么我们不深入讨论这个选项。
  • 你可以使用构造方法的静态属性缓存。函数在JavaScript中是对象,所以他们可以有属性。你可以这样Universe.instance并将对象缓存。这种是好的,简单解决办法,只有一个缺点,instance属性是公开被访问的,在你代码之外可能被改变,那么你就丢掉了那个实例。
  • 你可以用一个闭包包裹你的实例。这样可以保持instance私有,且在你的构造方法之外是不可被修改的,代价是需要一个额外的闭包。
让我们看一下第二个和第三个选项实现的例子。

静态属性实例(Instance in a Static Property)

这里是一个用Universe构造方法的静态属性缓存唯一对象的例子:
function Universe() {
    // do we have an existing instance?
    if (typeof Universe.instance === "object") {
        return Universe.instance;
    }
    // proceed as normal
    this.start_time = 0;
    this.bang = "Big";
    // cache
    Universe.instance = this;
    // implicit return:
    // return this;
}
// testing
var uni = new Universe();
var uni2 = new Universe();
uni === uni2; // true
如你所见,这是一个简单解决办法,唯一的缺点就是instance是公开(public)的。其它代码无意中修改它是不太可能的(和instance作为全局变量相比更少的可能性)但仍是可能的。

闭包中实例(Instance in a Closure)

另一种方法做到和class-like单例是使用闭包去保护唯一的对象。你可以通过私有静态成员模式(private static member pattern)实现。这里的密码武器就是重写构造方法:
function Universe() {
    // the cached instance
    var instance = this;
    // proceed as normal
    this.start_time = 0;
    this.bang = "Big";
    // rewrite the constructor
    Universe = function() {
        return instance;
    };
}
// testing
var uni = new Universe();
var uni2 = new Universe();
uni === uni2; // true
最初的构造方法第一次被调用并照例返回this。接下来第二次,第三次等等重写的构造方法被执行。重写的构造方法拥有通过闭包访问私有的instance且简单的返回它。

这个实现其实就是一个自定义函数模式的另一个例子。缺点,正如我们讨论的,重写的函数(在这种情况下Universe()构造函数)将会丢失在最初定义和重定义之间添加的任何属性。在我们特殊的例子中,你添加给Universe()原型的任何属性将不会和最初实现创建的实例有链接。

这里一些测试出来的问题:
// adding to the prototype
Universe.prototype.nothing = true;
var uni = new Universe();
// again adding to the prototype
// after the initial object is created
Universe.prototype.everything = true;
var uni2 = new Universe();
测试:
// only the original prototype was
// linked to the objects
uni.nothing; // true
uni2.nothing; // true
uni.everything; // undefined
uni2.everything; // undefined
// that sounds right:
uni.constructor.name; // "Universe"
// but that's odd:
uni.constructor === Universe; // false
uni.constructor不再和Universe()构造函数一样原因是因为uni.constructor仍指向最初的构造函数,不是重定义的函数。

如果让原型和构造函数指针如预期的那样工作是需要的,那是可以实现的通过一些微调:
function Universe() {
    // the cached instance
    var instance;
    // rewrite the constructor
    Universe = function Universe() {
        return instance;
    };
    // carry over the prototype properties
    Universe.prototype = this;
    // the instance
    instance = new Universe();
    // reset the constructor pointer
    instance.constructor = Universe;
    // all the functionality
    instance.start_time = 0;
    instance.bang = "Big";
    return instance;
}
现在所有的测试用例应该如预期那样工作:
// update prototype and create instance
Universe.prototype.nothing = true; // true
var uni = new Universe();
Universe.prototype.everything = true; // true
var uni2 = new Universe();
// it's the same single instance
uni === uni2; // true
// all prototype properties work
// no matter when they were defined
uni.nothing && uni.everything && uni2.nothing && uni2.everything; // true
// the normal properties work
uni.bang; // "Big"
// the constructor points correctly
uni.constructor === Universe; // true

一个可选的解决办法也是包裹构造方法和instance在一个立即执行函数中。第一次构造方法被调用,它创建一个对象并将私有的instance指向它。使用这种实现,前面代码段中的所有测试也都能如预期一样的工作:
var Universe; (function() {
    var instance;
    Universe = function Universe() {
        if (instance) {
            return instance;
        }
        instance = this;
        // all the functionality
        this.start_time = 0;
        this.bang = "Big";
    };
} ());















猜你喜欢

转载自blog.csdn.net/qq838419230/article/details/9750669