剑客
关注科技互联网

设计模式笔记(一):JavaScript中的单例模式

单例模式是一种常见的模式,如果 希望系统中一个类只有一个实例
,那么单例模式是最好的解决方案。

一. 单例模式的定义

单例模式的定义: 保证一个类仅有一个实例,并提供一个访问它的全局访问点。

二. 单例模式的实现原理

用一个变量来标志当前是否已经为某个类创建过对象。如果是,则在下一次获取该类的实例时,直接返回之前创建的对象。

三. 单例模式的优点

单例模式的优点有:

  • 内存中只有一个对象,节省内存空间;
  • 避免频繁销毁对象,提高性能;
  • 避免共享资源多重占用;
  • 可以全局访问。

四. 单例模式的实现方法

单例模式的核心是确保只有一个实例,并提供全局访问。因此,单例模式的实现都是围绕着如何确保实例的唯一性,因此需要用一个变量来标志当前是否已经为某个类创建过实例对象。

实现方法主要有以下四种:

  • 方法一: 可以 使用全局变量来存储该实例
    。但是全局变量容易被覆写。
  • 方法二: 可以 在构造函数的静态属性中缓存该实例
    。缺点是构造函数的静态属性是公开可访问的属性,在外部容易被覆写。
  • 方法三: 可以 将该实例包装在闭包中
    。这样可以保证该实例的私有性并保证该实例不会被构造函数之外的代码所修改。其代价是带来额外的闭包开销。
  • 方法四:可以 重写构造函数

这四种方法的具体实现如下:

4.1 使用全局变量保存单例

var instance;

function Cat() {
    if(typeof instance === "object") {
        return instance;
    }

    // Cat构造函数方法 ... ...

    instance = this;
}

/*============== 测试代码 ===============*/
var cat1 = new Cat();
var cat2 = new Cat();

console.log(cat1 === cat2); // true

使用全局变量来存储该实例对象是有风险的,不推荐使用这种方法。因为全局变量可以被任何人覆盖,容易被覆写,而使该实例对象丢失,从而导致意外事件。

应尽量减少全局变量的使用。即使需要,也应把它的污染降到最低。例如采用命名空间、使用闭包封装私有变量等方法,将全局变量带来的命名污染尽量降低。

4.2 在构造函数的静态属性中缓存实例

function Cat() {
    if(typeof Cat.instance === "object") {
        return Cat.instance;
    }

    // Cat构造函数方法 ... ...

    // 使用构造函数的静态属性来缓存实例
    Cat.instance = this;
}

/*============== 测试代码 ===============*/
var cat1 = new Cat();
var cat2 = new Cat();

console.log(cat1 === cat2); // true

这是一个非常直接的解决方法,其唯一的缺点在于构造函数的静态instance属性是公开的。一旦其他代码无意间修改了该属性,则有可能导致意外发生。

4.3 使用闭包创建对象

var Cat = (function() {
    var instance,
        _this = this;

    return function() {
        if(typeof instance !== "object") {
            // Cat构造函数方法 ... ...
            instance = _this;
        }
        return instance;
    }
})();

/*============== 测试代码 ===============*/
var cat1 = new Cat();
var cat2 = new Cat();

console.log(cat1 === cat2); // true

4.4 重写构造函数

function Cat() {
    var instance = this;

    // Cat构造函数方法 ... ...

    // 重写构造函数
    Cat = function() {
        return instance;
    }
}

/*============== 测试代码 ===============*/
var cat1 = new Cat();
var cat2 = new Cat();
console.log(cat1 === cat2); // true

这种模式的缺点,主要在于重写构造函数中,构造函数会丢失所有在初始定义和重定义时刻之间添加到它里面的属性。

在上述代码中,在重写构造函数后,任何添加到Cat()的原型中的对象都不会存在指向由原始构造函数所创建的实例的指针。如下:

/*============== 测试代码 ===============*/
// 向原型中添加属性
Cat.prototype.color = "white";

var cat1 = new Cat();

// 创建初始化对象后,再次向该原型添加属性
Cat.prototype.age = "1";

var cat2 = new Cat();

console.log(cat1 === cat2); // true
console.log(cat1.color); // white
console.log(cat2.color); // white
console.log(cat1.age); // undefined
console.log(cat2.age); // undefined
console.log(cat1.constructor === Cat); // false
console.log(cat2.constructor === Cat); // false

cat1.constructor不再与Cat()构造函数相同,是因为cat1.constructor指向了原始的构造函数,而不是重新定义的那个构造函数。所以在重写构造函数后,任何添加到Cat()的原型中的对象都不会存在指向由原始构造函数所创建的实例的指针。

如果希望在重写构造函数后,使原型中的对象指向原始构造函数,可以做以下调整:

function Cat() {
    var instance = this;

    // 重写构造函数
    Cat = function() {
        return instance;
    }

    // 保留原型属性
    Cat.prototype = this;
    // 创建实例
    instance = new Cat();
    // 重置构造函数指针
    instance.contructor = Cat;

    // Cat构造函数方法 ... ...

    return instance;
}

/*============== 测试代码 ===============*/
// 向原型中添加属性
Cat.prototype.color = "white";

var cat1 = new Cat();

// 创建初始化对象后,再次向该原型添加属性
Cat.prototype.age = "1";

var cat2 = new Cat();

console.log(cat1 === cat2); // true
console.log(cat1.color); // white
console.log(cat2.color); // white
console.log(cat1.age); // 1
console.log(cat2.age); // 1

五. 单例模式的优化与实际应用

5.1 使用闭包封装构造函数和实例

将构造函数和实例封装在几时执行函数中,这样在第一次调用构造函数时,它会创建一个对象,并且使得私有instance指向该对象。从第二次调用之后,该构造函数仅返回该私有变量。这样不仅可以避免全局变量污染,也可以实现单例。

var Cat;
(function() {
    var instance;

    Cat = function() {
        if(instance) {
            return instance;
        }

        // 使用构造函数的静态属性来缓存实例
        instance = this;

        // Cat构造函数的功能实现 ... ...
        // ... ...
    };
})();

/*============== 测试代码 ===============*/
var cat1 = new Cat();
var cat2 = new Cat();
console.log(cat1 === cat2); // true

5.2 用代理实现单例模式

可以引入代理类,在代理中实现一个类只能初始化一个实例。

function Cat() {
    // Cat构造函数方法 ... ...
}

var ProxyCat = (function() {
    var instance;
    return function() {
        if(typeof instance !== "object") {
            instance = new Cat();
        }
        return instance;
    }
})();

/*============== 测试代码 ===============*/
var cat1 = new ProxyCat();
var cat2 = new ProxyCat();

console.log(cat1 === cat2); // true

引入代理类的方式,跟之前不同的是,把负责管理单类的逻辑移到了代理类中。这样Cat就变成了一个普通的类。

5.3 通用的惰性单例

将管理单例的逻辑从原来的代码中抽离出来,并将这些逻辑封装在getSingle函数内部,创建对象的方法fn被当成参数动态传入getSingle函数:

var getSingle = function(fn) {
    var result;
    return function() {
        return result || (result = fn.apply(this, arguments));
    }
}

备注:该代码摘抄自《JavaScript设计模式与开发实践》第四章 P68。

这样,Cat的单例模式代码可以改写为:

function Cat() {
    // Cat构造函数方法 ... ...
}

var createCat = getSingle(Cat);

/*============== 测试代码 ===============*/
var cat1 = createCat();
var cat2 = createCat();

console.log(cat1 === cat2); // true

在这个例子中,把创建实例对象的职责和管理单例的职责分别放在两个方法中,这两个方法可以独立变化而互不影响,并实现了创建唯一实例对象的功能。推荐使用。

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址