单例模式在前端的妙用

146 阅读4分钟

单例模式的特点和应用场景

1. 两大特点

  • 保证一个类仅有一个实例
  • 并提供一个访问它的全局访问点

2. 工作开发场景

适用于系统中唯一的模块, 例如当我们单击登录按钮的时候,页面中会出现一个登录浮窗,而这个登录浮窗是唯一的, 那么适合单例模式.

如何更好的利用单例模式

1. 简单的单例实现

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

var Singleton = function( name ){
    this.name = name;
};
Singleton.prototype.getName = function(){
    alert ( this.name );
};
Singleton.getInstance = (function(){
    var instance = null;
    return function( name ){
        if ( !instance ){
            instance = new Singleton( name );
        }
        return instance;
    }
})();

var a = Singleton.getInstance( 'modA' ); 
var b = Singleton.getInstance( 'modB' ); 
alert ( a === b ); // true

上述代码虽然实现了单例, 但是没有意义,只是简单的说明一下前端创建单例的一种方式, 工作中几乎使用不到, 现在我们来一些工作中需要用到的单例模式.

2. 通过单例创建唯一的div dom元素

// 创建一个唯一的div标签
let CreateDiv = (function () {
    // 就是保存CreateDiv的实例, 如果有下次直接返回
    let instance = null; 

    class CreateDiv {
        constructor(html) {
            this.html = html;
            // 判断是否已经实例过
            if (instance){
                return instance;
            }
            this.init()
            // 构造函数里的this就是实例, 这里将实例保存到了instance中
            // instance = new CreateDiv(html); 这是错误的会没有出口一直执行
            console.log(this);
            instance = this;
        }

        init() {
            let div = document.createElement('div');
            div.innerHTML = this.html;
            document.body.append(div);
        }
    }

    return CreateDiv;

})()

let a = new CreateDiv('1')
let b = new CreateDiv('2')
console.log(a === b); // true

上述代码虽然实现了能创建唯一div功能, 但是CreateDiv方法处理了过多的事情, 一是创建对象和执行初始 化init方法,第二是保证只有一个对象。

工组中大家都很容易遇到上午定的需求下午就改的情况, 例如现在这个类不能只创建单个div, 能够创建很多个div。如果按照上述代码则需要进行修改createDiv这个类, 那么也就违背了封闭原则。

其实解决思路也很简单, 就是通过上篇文章的代理模式和单例模式进行结合处理, 我们暂时称为代理加单例结合模式吧!

// 创建div方法类
class CreateDiv {
    constructor(html) {
        this.html = html;
        this.init()

    }
    init() {
        let div = document.createElement('div');
        div.innerHTML = this.html;
        document.body.append(div);
    }
}

// 通过代理成为单例类 - 后期还有代理说明
let ProxySingleCreateDiv = (function () {
    let instance;
    return function (html) {
        if (!instance) {
            instance = new CreateDiv(html);
        }
        return instance
    }
})();

let a = new ProxySingleCreateDiv('1');
let b = new ProxySingleCreateDiv('2');

console.log(a === b); TRUE

通过代理模式进行了解耦, 在开发中就获得了更加容易复用的代码, 例如, 我想创建多个div就使用createDiv这个类方法即可, 如果我想创建单个div那么就把它们结合起来用。 还是强烈推荐一波JS中的代理模式文章。

单例的其他运用: 惰性单例、 唯一事件绑定机制

1. 惰性单例

系统中的一些不常用的模块, 没有必要一开始就加载, 而是等待用户触发事件后才去加载。例如: 退出登录的弹窗模块:

<button id="open">打开</button>
<button id="close">关闭</button>

let CreateDialog = (function () {
    let dialog;

    return function () {
        if (!dialog) {
            console.log('一次就好');
            dialog = document.createElement('div');
            dialog.innerHTML = '我是弹窗';
            dialog.style.display = 'none';
            document.body.append(dialog)
        }
        return dialog;
    }

})();


let btnOpen = document.getElementById('open');
let btnClose = document.getElementById('close');

btnOpen.onclick = function () {
    let dialog = CreateDialog();
    dialog.style.display = 'block'
}
btnClose.onclick = function () {
    let dialog = CreateDialog();
    dialog.style.display = 'none'
}

我们再一次分析上述代码问题:

  1. 违反单一原则, 创建弹窗和单例逻辑都放在一起了。
  2. 不通用, 如果下次创建别的标签, 那么所有逻辑都要重写抄写一边。

还是一样,通过代理 + 单例模式进行优化:

// 单例职责
let getSingle = function (fn) {
    let result;
    return function () {
        return result || (result = fn.apply(this, arguments))
    }
};
// 创建弹窗职责
let createLoginLayer = function(){
    let div = document.createElement( 'div' );
    div.innerHTML = '我是登录浮窗';
    div.style.display = 'none';
    document.body.appendChild( div );
    return div;
};

// 结合返回创建登录浮窗单例
// 这两个方法可以独立变化而互不影响
let createSingleLoginLayer = getSingle( createLoginLayer );


let btnOpen = document.getElementById('open');
let btnClose = document.getElementById('close');
btnOpen.onclick = function () {
    let dialog = createSingleLoginLayer();
    dialog.style.display = 'block'
}
btnClose.onclick = function () {
    let dialog = createSingleLoginLayer();
    dialog.style.display = 'none'
}

2. 唯一绑定事件

工作中我们通常封装dom的绑定事件, 但是没有结合单例模式的话可能会造成一个事件的多次绑定。 如:

// 此时这个button绑定了三个, 点击一下打印3个1
function bind() {
    let btnOpen = document.getElementById('open');
    btnOpen.addEventListener('click', function () {
        console.log(1);
    }, false);
}

function render() {
    bind();
}

render()
render()
render()

使用单例优化

// 单例绑定,多次render只会绑定一次
let getSingle = function (fn) {
    let result;
    return function () {
        return result || (result = fn.apply(this, arguments));
    }
}

// 结合返回单例绑定事件
let bind = getSingle(function () {
    let btnOpen = document.getElementById('open');
    btnOpen.addEventListener('click', function () {
        console.log(1);
    }, false);
    return true; // 返回一个true下次就不会走这个方法
})

function render() {
    bind();
}

render()
render()
render()

结语

通过几个小小的案例来展示单例模式和代理模式的结合使用, 当然工作中的情况可能并非这么轻松, 只有了解了更多相关设计知识并且进行大量尝试才知道应该怎样将设计模式运用到工作中。 以后还会继续分享, 希望能帮助到大家。