读书笔记-JavaScript设计模式与开发实践-单例模式

单例模式的核心是确保只有一个单例,并且提供全局访问。标准的单例模式并不复杂,只是用一个变量来标识当前是否已为某个类创建过对象,创建过的话下一次获取该类的实例时返回之前创建的对象。js中,线程池、全局缓存、window对象都是只有一个的,适用单例模式。

1 透明的单例模式

用户从这个单例模式中创建对象的时候可以像使用其他任何普通类一样。下面创建一个CreateDiv单例类,负责在页面中创建唯一的div节点。

let CreateDiv = (function () {
let instance;
let CreateDiv = function (html) {
if (instance) {
return instance;
}
this.html = html;
this.init();
return instance = this;
};

CreateDiv.prototype.init = function () {
let div = document.createComment('div');
div.innerHTML = this.html;
document.body.appendChild(div);
};

return CreateDiv;
})();

let a = new CreateDiv('sven1')
let b = new CreateDiv('sven2')

alert(a === b) //true

这段代码里,CreateDiv只负责创建对象和初始化init方法、保证只有一个对象。但是为了封装instance使用的自执行匿名函数和闭包,并且让匿名函数返回真正的Singleton方法,难以阅读。而且要是想将单例类变为普通的产生多个实例的类就需要修改CreateDiv构造函数了,下面通过引入代理类解决这个问题。

2 用代理实现单例模式

引入代理类解决上述问题,首先构造普通的创建div的CreateDiv类,然后引入代理类;把管理单例的逻辑放到代理类ProxySingletonCreateDiv中实现,这样两个类组合起来就是单例模式的效果。本例子也是缓存代理的应用之一。

let CreateDiv = function (html) {
this.html = html;
this.init();
};

CreateDiv.prototype.init = function () {
let div = document.createComment('div');
div.innerHTML = this.html;
document.body.appendChild(div);
};

let ProxySingletonCreateDiv = (function () {
let instance;
return function (html) {
if (!instance) {
instance = new CreateDiv(html)
}
return instance
}
})()

let a = new ProxySingletonCreateDiv('seven')
let b = new ProxySingletonCreateDiv('seven1')

alert(a === b)

3 JavaScript中的单例模式

前面的实现是按照面向对象语言来的,其实JavaScript中实现单例模式,可以用let或者const在声明全局变量就可以实现,本书中因为时间较久远,var声明的变量二次声明会被覆盖,所以不能用来实现单例模式,但是let、const没有这个问题,但是仔细想想也不对,let、const是块级作用域,那在别的函数里就可以重新实现/修改这个单例了,就会导致后续单例结果不一样。还是使用闭包封装或者命名空间的办法好一点,闭包封装私有变量代码如下,只暴露接口和外界通信。

let user = (function () {
let _name = 'xinyu', _age = 24;
return {
getUserInfo: function () {
return _name + _age;
}
}
})();

4 通用的惰性单例

惰性单例是单例模式的重点,实际开发中应用极多。惰性是指需要调用的时候再创建,而不是页面加载好的时候就创建了,惰性单例可以优化页面加载速度。比如一个登陆弹窗,产品功能不需要上来就登陆,惰性的话,不随着页面一起加载登陆部分节约资源,然后点击登陆按钮,单例的话多次点击登陆按钮也只会创建一个登陆弹窗,进一步优化资源,惰性单例模式就很适合实现这个功能。

实现通用的惰性单例:

  1. 隔离出不变的部分,管理单例的逻辑可以抽象出来。逻辑是:用一个变量来标识是否创建过对象,如果是,下次直接返回这个对象。
    let obj;
    if(!obj){
    obj = ...
    }
  2. 抽离出如何管理单例的逻辑,逻辑封装在getSingle函数里面,fn被当成参数传入getSingle函数。
    let getSingle = function (fn) {
    let result;
    return function () {
    return result || (result = fn.apply(this, arguments))
    // result存在就返回result,否则就让result等于传入的参数并返回,fn.apply(this, arguments))
    //为了更改this指向到createLoginLayer,否则loginLayer.style.display = 'block';报错
    //**重要,自己理解为在this里运行fn,参数为传入arguments数组中的每一个元素
    //(或者把arguments传入fn中,然后把结果加入到this中)
    //或者去看下这个的apply部分https://segmentfault.com/a/1190000019970715
    }
    }
  3. 将用于创建登陆弹窗的方法用参数fn的方式传入getSingle,(也能传入别的创建同等的单例)之后让一个新的函数用变量result来保存fn的计算结果,result变量因为在闭包中,永远不会被销毁,以后请求result,如果之前result已经被赋值,将返回这个值。
    let createLoginLayer = function () { //创建登陆弹窗的方法
    let div = document.createElement('div');
    div.innerHTML = '我是登陆窗口';
    div.style.display = 'none';
    document.body.appendChild(div);
    return div;
    }

    let createSingleLoginLayer = getSingle(createLoginLayer);
    document.getElementById('loginBtn').onclick = function () {
    let loginLayer = createSingleLoginLayer();
    loginLayer.style.display = 'block';
    }
文章作者: 鐔雨
文章链接: https://caichunyu.github.io/2021/12/09/读书笔记-JavaScript设计模式与开发实践-单例模式/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 鐔雨的Blog