定义

限制类实例化次数只能一次,一个类只有一个实例,并提供一个访问它的全局访问点。适用于单一对象,只生成一个对象实例,避免频繁创建和销毁实例,减少内存占用。不适用动态扩展对象,或需创建多个相似对象的场景。

原理

使用一个变量存储类实例对象,初始值为 null或者undefined 。进行类实例化时,首先判断类实例对象是否存在,存在则返回该实例,不存在则创建类实例后返回。无论调用多少次类生成实例方法,返回的都是同一个实例对象。

类图

【设计模式】前端这样学习设计模式-单例模式_json

简单单例模式

一个类可以创建多个实例,并且每个实例之间都不相等

class Window {
  constructor() {}
}

let w1 = new Window();

let w2 = new Window();

//两个实例不相等
console.log(w1 === w2); //false
复制代码

那我们想实现单例怎么办,把constructor设成私有,这样就不能在外部访问构造函数了。在函数 SingleObject 中定义一个getInstance()方法来管控单例,并创建返回类实例对象,而不是通过传统的 new 操作符来创建类实例对象。第一次调用Window.getInstance()没有值,那么初始化一个值并返回。第二次有值直接返回。这样就不可以创建两个Window的实例。

es6实现单例模式

/**
*使用static关键字定义静态属性  通过类访问
*TS中属性具有三种修饰符:
*public(默认值),可以在类、子类和对象中修改
*protected ,可以在类、子类中修改
*private ,可以在类中修改
 */
class Window {
  private static instance: Window;
  private constructor() {}
  public static getInstance() {
    if (!Window.instance) {
      Window.instance = new Window();
    }
    return Window.instance;
  }
}

let w1 = Window.getInstance();

let w2 = Window.getInstance();

console.log(w1 === w2); //true
复制代码

es5实现的单例模式

interface Window {
    hello: any
}
function Window() { }
Window.prototype.hello = function () {
    console.log('hello');
}
Window.getInstance = (function () {
    let window: Window;
    return function () {
        if (!window)
            window = new (Window as any)();
        return window;
    }
})();
let window = Window.getInstance();
window.hello();
复制代码

这种方式有个缺点,必须告诉使用者通过getInstance()方法来得到单例。

透明单例模式

使用者不知道要按照单例使用,还是会使用new来调用构造函数从而发生错误,那么为了解决这个问题,我们通过下面这种方式,还是使用new的方式创建对象,但返回的都是同一个实例。

let Window = (function () {
  let window: Window;
  let Window = function (this: Window) {
    if (window) {
      return window;
    } else {
      return (window = this);
    }
  };
  Window.prototype.hello = function () {
    console.log('hello');
  };
  return Window;
})();

let window1 = new (Window as any)();
let window2 = new (Window as any)();
window1.hello();
console.log(window1 === window2);//true
复制代码

这样使用者就不需要知道该怎么特殊使用了,正常new就行了。

单例与构建分离

上面的例子创建单例的方法都在类的内部实现的,管理单例的操作,与对象创建的操作,功能代码耦合在一起,不符合 “单一职责原则”。所以我们要进行单例与构建的分离。

interface Window {
    hello: any
}
function Window() {
}
Window.prototype.hello = function () {
    console.log('hello');
}

let createInstance = (function () {
    let instance: Window;
    return function () {
        if (!instance) {
            instance = new (Window as any)();
        }
        return instance;
    }
})();

let window1 = createInstance();
let window2 = createInstance();
window1.hello();
console.log(window1 === window2)
复制代码
封装变化

上面的例子createInstance只能创建Window的实例,我希望createInstance可以创建任何类的实例


function Window() {

}
Window.prototype.hello = function () {
    console.log('hello');
}
let createInstance = function (Constructor: any) {
  	//创建变量 任何类型
    let instance: any;
    return function (this: any) {
        if (!instance) {
            Constructor.apply(this, arguments);
          	//this.__proto__ = Constructor.prototype
            Object.setPrototypeOf(this, Constructor.prototype)
            instance = this;
        }
        return instance;
    }
};
let CreateWindow: any = createInstance(Window);
let window1 = new CreateWindow();
let window2 = new CreateWindow();
window1.hello();
console.log(window1 === window2)
复制代码
示例

模态窗口的实现

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>

<body>
    <button id="show-button">显示模态窗口</button>
    <button id="hide-button">隐藏模态窗口</button>
    <script>
        class Login {
            constructor() {
                this.element = document.createElement('div');
                this.element.innerHTML = (
                    `
            用户名 <input type="text"/>
            <button>登录</button>
            `
                );
                this.element.style.cssText = 'width: 100px; height: 100px; position: absolute; left: 50%; top: 50%; display: block;';
                document.body.appendChild(this.element);
            }
            show() {
                this.element.style.display = 'block';
            }
            hide() {
                this.element.style.display = 'none';
            }
        }
        Login.getInstance = (function () {
            let instance;
            return function () {
                if (!instance) {
                    instance = new Login();
                }
                return instance;
            }
        })();

        document.getElementById('show-button').addEventListener('click', function (event) {
            Login.getInstance().show();
        });
        document.getElementById('hide-button').addEventListener('click', function (event) {
            Login.getInstance().hide();
        });
    </script>
</body>

</html>
复制代码

缓存

访问磁盘文件是异步的,每次都调用readFile方法会很慢,我们如果已经访问过了,就添加缓存到内存里。 访问内存比访问磁盘要快的多。

let express = require('express');
let fs = require('fs');
let app = express();
app.get('/user/:id', function (req: any, res: any) {
  let id = req.params.id  
  fs.readFile(`./users/${id}.json`, 'utf8', function (err: any, data: any) {
    let user = JSON.parse(data);
    res.json(user);
  });
});
app.listen(3000);
复制代码

缓存一定要做成单例的,这样不管谁存进去了,别人都可以拿到

let express = require('express');
let fs = require('fs');
//缓存
let cache: Record<any, any> = {};
let app = express();
app.get('/user/:id', function (req: any, res: any) {
    let id = req.params.id;
  	//如果缓存里有直接返回  否则访问磁盘
    let user = cache.get(id);
    if (user) {
        res.json(user);
    } else {
        fs.readFile(`./users/${id}.json`, 'utf8', function (err: any, data: any) {
            let user = JSON.parse(data);
            cache.put(id, user);
            res.json(user);
        });
    }
});
app.listen(3000);
复制代码