什么是单例模式 保证一个类仅有一个实例,并提供一个访问它的全局访问点,这样的模式就是单例模式,也是单例模式的定义。
单例模式的用途非常广,像实现全局模态框,像在Redux和Vuex中的Store实现,就是单例模式的典型应用。
实现简单的单例模式 实现一个简单的单例模式并不复杂,核心知识点在于使用一个变量来标志这个类是否已经创建过对象实例,如果是,则直接返回之前创建的对象实例,如果为否,则创建这个类的对象实例,并保存到这个变量标志中。
代码实现 ES5实现 完整代码 1 2 3 4 5 6 7 8 9 10 11 12 13 var Singleton = function (name ) { this .name = name; this .instance = null ; }; Singleton.prototype.getName = function ( ) { console .log(this .name); }; Singleton.getInstance = function (name ) { if (!this .instance) { this .instance = new Singleton(name); } return this .instance; };
测试用例 1 2 3 let a = Singleton.getInstance( 'sven1' );let b = Singleton.getInstance( 'sven2' );console .log ( a === b );
ES6实现 完整代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Singleton { constructor (name ) { this .name = name; this .instance = null ; } getName ( ) { console .log(this .name); } static getInstance (name ) { if (!this .instance) { this .instance = new Singleton(name); } return this .instance; } }
测试用例 1 2 3 let a = Singleton.getInstance( 'sven1' );let b = Singleton.getInstance( 'sven2' );console .log ( a === b );
透明的单例模式 上面的例子中,我们实现了简单的单例模式,虽然这种方法使用起来很简单,但是对使用者而言,需要提前知道这个类是一个**”单例类”,才知道该使用 getInstance**来获取对象实例。
为了实现单例模式的”透明”特性,在ES5中,我们要用上自执行匿名函数和闭包,利用闭包存储公共的单例类实例,使用自执行匿名函数返回真正的单例类的构造方法;而在ES6中,我们可以利用类的构造器返回公共的单例类实例。
代码实现 ES5实现 完整代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 var CreateDiv = (function ( ) { var instance; var CreateDiv = function ( html ) { if (instance){ return instance; } this .html = html; this .init(); return instance = this ; }; CreateDiv.prototype.init = function ( ) { var div = document .createElement( 'div' ); div.innerHTML = this .html; document .body.appendChild( div ); }; return CreateDiv; })();
测试用例 1 2 3 4 5 var a = new CreateDiv( 'sven1' );var b = new CreateDiv( 'sven2' );console .log(a === b);
ES6实现 完整代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class CreateDiv { constructor (html ) { if (!CreateDiv.instance) { CreateDiv.instance = this ; this .html = html; this .init(); } return CreateDiv.instance; } init () { var div = document .createElement( 'div' ); div.innerHTML = this .html; document .body.appendChild( div ); }; }
测试用例 1 2 3 4 5 let a = new CreateDiv( 'sven1' );let b = new CreateDiv( 'sven2' );console .log(a === b);
用代理实现单例模式 虽然上述这种透明的单例类写法可以让使用者直接使用new关键字的获取单例类实例,但是一定程度增加了程序阅读的复杂度,还有可能破坏了”单一职责原则”。如果以后需要为其他类实现透明的单例,就要重复为这个类重新实现一遍透明单例类,这怎么看都不是一种很好的解决方法。
利用ES6的代理(proxy),来解决这个问题,就可以让单例实现和类本身进行解耦。在ES5中,由于并不支持代理(proxy)的特性,我们需要自己实现一个代理类。
代码实现 ES5实现 完整代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 var CreateElement = function ( html ) { this .html = html; this .init(); }; CreateElement.prototype.init = function ( ) { var div = document .createElement( 'div' ); div.innerHTML = this .html; document .body.appendChild( div ); };var ProxySingletonCreateElement = (function (CreateElement ) { var instance; return function ( ) { if (!instance) { instance = new CreateElement(...arguments) } return instance; } })(CreateElement);
测试用例 1 2 3 4 var a = new ProxySingletonCreateElement( 'sven1' );var b = new ProxySingletonCreateElement( 'sven2' );console .log ( a === b );
ES6实现 完整代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 class CreateDiv { constructor (html ) { this .html = html; this .init(); } init ( ) { let div = document .createElement("div" ); div.innerHTML = this .html; document .body.appendChild(div); } }function singletonProxy (className ) { let instance = null ; return new Proxy (className, { construct (target, args ) { class ProxyClass { constructor ( ) { if (instance === null ) { instance = new target(...args); } return instance; } } return new ProxyClass(); }, }); }
测试用例 1 2 3 4 5 let createSothx = singletonProxy(CreateDiv)let sothx1 = createSothx('sothx1' )let sothx2 = createSothx('sothx2' )console .log(sothx1 === sothx2)
JavaScipt中的单例模式 对于传统面向对象语言而言,单例对象是从”类”中创建而来的,比如Java中,如果需要某个对象,就必须先定义一个类,对象从类中被创建出来。
但是对于JavaScript而言,其实是一门无类(class-free)语言,因此在JavaScript中创建对象的方法特别简单,只需要保证单例模式的核心——确保只有一个实例,并提供全局访问。
那么他就是一个单例类。
直接在全局上下文定义一个对象字面量的全局变量,也可以把它看做单例类,但是全局变量很容易造成命名空间污染的问题。
模拟命名空间,可以减少全局变量造成污染的概率。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 let namespace1 = { a: function ( ) { console .log(1 ); }, b: function ( ) { console .log(2 ); } };let MyApp = {}; MyApp.namespace = function ( name ) { var parts = name.split( '.' ); var current = MyApp; for ( var i in parts ){ if ( !current[ parts[ i ] ] ){ current[ parts[ i ] ] = {}; } current = current[ parts[ i ] ]; } }; MyApp.namespace( 'event' ); MyApp.namespace( 'dom.style' );console .dir( MyApp ); let MyApp = { event: {}, dom: { style: {} } };let user = (function ( ) { let __name = 'sven' , __age = 29 ; return { getUserInfo: function ( ) { return __name + '-' + __age; } } })();
在ES6中,我们可以模块(module)来模拟命名空间。
创建一个模块文件
1 2 3 4 5 6 7 8 9 10 let MyApp = { event: {}, dom: { style: {} } };export default MyApp;
在页面 中引入模块
1 <script type ="module" src ="./MyApp.js" > </script >
在需要使用该模块的js文件 中导入模块并使用
1 2 3 import MyApp from './MyApp.js' ;console .log(MyApp)
懒汉式和饿汉式 单例模式的写法有很多,懒汉式,饿汉式,注册式,序列化式等等……
这里着重对懒汉式单例和饿汉式单例做一个比较讲解。
懒汉式:默认不会实例化,什么时候用到了,才会创建对象实例。
饿汉式:类加载的时候就会进行实例化,并且创建单例对象实例。
像先前的例子,Singleton.getInstance在调用的时候才会被创建,这种就是典型的懒汉式单例。
1 2 3 4 5 6 7 8 9 Singleton.getInstance = (function ( ) { var instance = null ; return function ( name ) { if ( !instance ){ instance = new Singleton( name ); } return instance; } })();
用在实际的例子,比如创建一个登录弹框,我们在JavaScript也可以这样做。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 var createLoginLayer = (function ( ) { var div; return function ( ) { if ( !div ){ div = document .createElement( 'div' ); div.innerHTML = '我是登录浮窗' ; div.style.display = 'none' ; document .body.appendChild( div ); } return div; } })();document .getElementById( 'loginBtn' ).onclick = function ( ) { var loginLayer = createLoginLayer(); loginLayer.style.display = 'block' ; };
通用的懒汉式单例 上面的登录弹框写法虽然可用,但是违反了设计模式的”单一职责原则”,创建对象和管理单例的逻辑都耦合在了一起。如果之后要创建更多的单例,就要重复复制粘贴一遍又一遍单例代码。
所以我们需要把管理单例的逻辑从原来的代码中抽离出来。
1 2 3 4 5 6 let getSingle = function ( fn ) { let result; return function ( ) { return result || ( result = fn.apply(this , arguments ) ); } };
接着,无论有多少种不同的创建实例对象的方法,getSingle这个管理管理方法都能很好的完成任务。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 let createLoginLayer = function ( ) { var loginDiv = document .createElement( 'div' ); loginDiv.innerHTML = '我是登录浮窗' ; loginDiv.style.display = 'none' ; document .body.appendChild( loginDiv ); return loginDiv; };var createSingleLoginLayer = getSingle( createLoginLayer );document .getElementById( 'loginBtn' ).onclick = function ( ) { var loginLayer = createSingleLoginLayer(); loginLayer.style.display = 'block' ; };let createSingleIframe = getSingle( function ( ) { let iframe = document .createElement ('iframe' ); document .body.appendChild(iframe); return iframe; });document .getElementById( 'loginBtn' ).onclick = function ( ) { var loginLayer = createSingleIframe(); loginLayer.src = 'http://baidu.com' ; };let bindEvent = getSingle(function ( ) { document .getElementById( 'div1' ).onclick = function ( ) { console .log ( 'click' ); } return true ; });var render = function ( ) { console .log( '开始渲染列表' ); bindEvent(); }; render(); render(); render();
参考资料 《JavaScript设计模式与开发实践》——单例模式
https://www.ituring.com.cn/book/1632
Proxy代理实现单例模式
https://juejin.cn/post/6844903929885507598