文章初衷
设计模式其实旨在解决语言本身存在的缺陷
目前javaScript一些新的语法特性已经集成了一些设计模式的实现,
大家在写代码的时候,没必要为了用设计模式而去用设计模式,
那么我这边为什么还写设计模式的文章呢,
一方面是自己的一个整理,然后记录出来,结合自己的理解,
一方面就是虽然语言特性本身已经实现这些模式,有了自己的语法,
但是我们何尝不能去了解一下它是通过什么样的思路去实现了
在我看来设计模式更多的是让我对于思考问题,有了一些更好的思路和想法
文章实现更多的表现为用一些简单的案例,帮助大家去理解这样的一种思路,
会存在故意把设计模式的实现往简单的案例靠拢,
大家在真实项目中不要刻意去用设计模式实现相同的代码
设计模式在平时的一些代码中都会有所体现,大家也许经常用到,
耐心看文章,也许你会发现自己平时的代码就不断在设计模式中体现
JavaScript设计模式系列
JavaScript设计模式系列,讲述大概20-30种设计模式在JavaScript中的运用
后面对应的篇幅会陆续更新,欢迎大家提出建议
这是设计模式系列第三篇,讲述单例模式
注意
深入系列文章部分是有先后顺序的,按照目录结构顺序阅读效果最好。
勘误及提问
如果有疑问或者发现错误,可以在相应的 issues 进行提问或勘误。
单例模式
概念:
单例模式主要目的是确保系统中某个类只存在唯一一个实例,
也就是说对于这个类的重复实例的创建始终只返回同一个实例
在JavaScript里,单例可以作为一个命名空间提供者,
从全局命名空间里提供一个唯一的访问点来访问该对象
需要解决的问题
1.就拿我们常玩的超级玛丽游戏来说,游戏中小怪会不断出现,但是超级玛丽只有一个,如果游戏中出现多个玛丽实例的话,那就会出现问题,这时候就会用到我们所说的单例模式
2.再比如说我们页面窗口的弹窗,弹窗大部分情况下我们只需要一个,如果同时出现多个错误弹窗的话,网页就会显示的很奇怪,这时候弹窗的创建也会用到我们所说的单例模式
案例体现
那我们下面就来看一下单例模式在代码中的体现
对于 Java 之类的静态语言而言,实现单例模式常见的方法就是
将构造函数私有化,对外提供一个比如名为getInstance方法
静态接口来访问初始化好的私有静态的类自身实例。
但对于 JavaScript这样的动态语言而言,
单例模式的实现其实可以很灵活,
因为 JavaScript 语言中并不存在严格意义上的类的概念,只有对象。
每次创建出的新对象都和其他的对象在逻辑上不相等,
即使它们是具有完全相同的成员属性的同构造器创造出的对象。
所以,在 JavaScript 中,最常见的单例模式莫过于全局变量了:
这边就拿上面提到的超级玛丽游戏举一个简单的🌰
// 游戏人物
var Player = {
hp: 100,
mp: 200
}复制代码
可见,全局变量就是一种最简单最常见的单例模式了。在全局的其他地方要获得这个单例的对象,其实就是获得这个唯一
的全局变量,就可以保证访问的是同一实例了
之前提过在JavaScript里,单例可以作为一个命名空间提供者,
从全局命名空间里提供一个唯一的访问点来访问该对象
那么我们下面来看一下具体实现
命名空间概念:
命名空间就是所谓的namespace,它解决这么一类问题:
为了让代码更易懂,人们常常用单词或者拼音定义变量或者方法,
但是因为可用的单词或者拼音有限,不同人定义的变量可能重复,
此时就需要用命名空间来约束每个人定义的变量来解决这类问题.
看一段代码
/**
* 命名空间
*/
var Main = {
run: function() {
console.log('run');
},
jump: function() {
console.log('jump');
}
}
Main.run();复制代码
这边定义了一个
Main
的命名空间,那么所有的属性还有方法都会放到
Main
这个命名空间下面,自然就可以保证变量名的唯一性.
jQuery
这边也是用的同样的方式,定义了一个jQuery
的命名空间,所以的
jQuery
方法还有属性都放到这个命名空间,这样就不会跟别的库的代码冲突
上面的命名空间案例仅仅是一个简单的对象,
但很多时候对象可能还涉及到初始化的工作,
可能需要实现按需加载(懒加载),对象中还会存在内部私有成员,
对外需以门面模式(Facade)提供可访问的接口。
所以我们还可以把这个再扩充一下:
之前有提过单例模式在Java中的实现方式,复习一下
对于 Java 之类的静态语言而言,实现单例模式常见的方法就是将
构造函数私有化,对外提供一个比如名为getInstance方法
静态接口来访问初始化好的私有静态的类自身实例。
我们这边尝试一下Java中的单例模式在javascript中的实现,看🌰
/ 游戏人物
var Player = (function() {
// 实例
var instance = null;
// 私有变量
var hp = 100; // 生命值
var mp = 200; // 魔法值
// 初始化函数
function init() {
return {
// 获取生命值
getHp: function() {
return hp;
},
// 获取魔法值
getMp: function() {
return mp;
},
// 行走方法
run: function() {
console.log('running');
}
}
}
return {
// 初始化一个游戏英雄
init: function() {
// 未存在,则初始化
if (!instance) {
instance = init();
}
return instance;
}
}
})();
// 创建一个游戏英雄
var person1 = Player.init();
console.log(person1.getHp());
console.log(person1.getMp());
// 死亡复活
var person2 = Player.init();
// 创建的还是之前的对象
console.log(person1 === person2); // true复制代码
这边创建一个Player类,利用立刻执行函数,
然后创建一个空的实例变量instance用来记录是否已经实例化,
创建2个私有变量hp,mp,创建一个初始化函数init,
利用闭包的方式,暴露对外访问的接口,
细心的人可以看到代码里面输出的person1 完全等于person2
因为提供了公有的静态方法init,负责检验实例的存在性并实例化自己,
然后存储在实例对象instance中,以确保只有一个实例被创建
init: function() {
// 未存在,则初始化
if (!instance) {
instance = init();
}
return instance;
}复制代码
在jQuery中的应用
(function( window, undefined ) {
var jQuery = (function() {
// 构建 jQuery 对象
var jQuery = function( selector, context ) {
return new jQuery.fn.init( selector, context, rootjQuery );
}
// jQuery 对象原型
jQuery.fn = jQuery.prototype = {
constructor: jQuery,
init: function( selector, context, rootjQuery ) {
// selector 有以下 7 种分支情况:
// DOM 元素
// body(优化)
// 字符串:HTML 标签、HTML 字符串、#id、选择器表达式
// 函数(作为 ready 回调函数)
// 最后返回伪数组
}
};
// 猜猜这句是干什么呢?
jQuery.fn.init.prototype = jQuery.fn;
// 合并内容到第一个参数中,后续大部分功能都通过该函数扩展
// 通过 jQuery.fn.extend 扩展的函数,大部分都会调用通过 jQuery.extend 扩展的同名函数
jQuery.extend = jQuery.fn.extend = function() {};
// 在 jQuery 上扩展静态方法
jQuery.extend({
// ready bindReady
// isPlainObject isEmptyObject
// parseJSON parseXML
// globalEval
// each makeArray inArray merge grep map
// proxy
// access
// uaMatch
// sub
// browser
});
// 到这里,jQuery 对象构造完成,后边的代码都是对 jQuery 或 jQuery 对象的扩展
return jQuery;
})();
window.jQuery = window.$ = jQuery;
})(window);复制代码
jQuery这边也采用了上面的方式创建了jQuery命名空间,
然后对应的属性还有方法都在这个命名空间下面扩展,
jQuery这边也把构造函数私有化,在调用的时候完成实例化,
不同点是jQuery每次都返回新的对象
单例模式的优点
由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能。
命名空间的管理方式,可以有效避免变量的重复,尽量避免使用全局变量
单例模式的缺点
单例模式实现使用了闭包,过多使用闭包会导致内存泄露
单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。
单例模式适用情况
系统只需要一个实例对象,或者需要考虑资源消耗太大而只允许创建一个对象。
在javascript中,把功能封装起来,形成一个小型代码库,就可以使用命名空间的方式去实现,按照模块化划分
单例模式总结
在javascript中,建议采用命名空间的方式去实现单例,
在命名空间下面扩展对应的属性还有方法,
以模块化的方式去完成对应的功能模块避免滥用单例模式,因为使用了闭包,过渡使用会导致内存泄露
注意
深入系列文章部分是有先后顺序的,按照目录结构顺序阅读效果最好。