设计模式是什么
- 设计模式就是前人总结出来的一套更加容易阅读、维护以及复用的写代码的方式
- 模式就是一个我们如何解决问题的模版
- 很多用起来顺手的开源库都有良好的封装,封装可以将内部环境和外部环境隔离,外部用起来更顺手。
- 针对不同的场景可以有不同的封装方案。
- 需要大量产生类似实例的组件可以考虑用工厂模式来封装。
- 内部逻辑较复杂,外部使用时需要的实例也不多,可以考虑用建造者模式来封装。
- 全局只能有一个实例的需要用单例模式来封装。
- 新老对象之间可能有继承关系的可以考虑用原型模式来封装,JS本身就是一个典型的原型模式。
- 使用设计模式时不要生搬硬套代码模板,更重要的是掌握思想,同一个模式在不同的场景可以有不同的实现方案。
- 用于代码封装的设计模式主要有四种:工厂模式、单例模式、原型模式、创建者模式
封装的好处
- 内部变量不会污染外部
- 可以作为一个模块给外部调用
- 对扩展开放,对修改关闭,即开闭原则。外部不能修改模块,既保证了模块内部的正确性,又可以留出扩展接口,使用灵活。
工厂模式
- 简单工厂模式就是由一个方法来决定到底该创建哪个类的实例,这些实例通常有相同的接口 ( 属性和方法 )
- 优点:你只需要一个正确的参数就能获取到你想要的对象
- 实例:提示弹窗的种类,jQuery 也是一个典型的工厂模式,你给他一个参数,他就给你返回符合参数 DOM 对象
function infoPopup(content, color) {}
function confirmPopup(content, color) {}
function cancelPopup(content, color) {}
// 新加一个方法popup把这几个类都包装起来
function popup(type, content, color) {
switch (type) {
case 'infoPopup':
return new infoPopup(content, color);
case 'confirmPopup':
return new confirmPopup(content, color);
case 'cancelPopup':
return new cancelPopup(content, color);
}
}
function popup(type, content, color) {
// 如果是通过new调用的,返回对应类型的弹窗
if (this instanceof popup) {
return new this[type](content, color);
} else {
// 如果不是new调用的,使用new调用,会走到上面那行代码
return new popup(type, content, color);
}
}
// 各种类型的弹窗全部挂载在原型上成为实例方法
popup.prototype.infoPopup = function (content, color) {}
popup.prototype.confirmPopup = function (content, color) {}
popup.prototype.cancelPopup = function (content, color) {}
单例模式 Singleton Pattern
- 单例就是保证一个类只有一个实例,实现的方法一般是先判断实例存在与否,如果存在直接返回,如果不存在就创建了再返回,这就确保了一个类只有一个实例对象。
- 把描述同一件事物的属性和特征进行 分组、归类 ( 存储在同一个堆内存空间中 ),因此避免了全局变量之间的冲突和污染
- 在单例模式中 object 不仅仅是对象名,它被称为命名空间 [ NameSpace ],把描述事物的属性放到命名空间中,多个命名空间是独立分开的互不冲突。
// 高级单例模式
const pattern1 = (function() {
let n = 'xxx'
function fn() {
//...
}
return {
fn
}
})()
// 一个自定义函数执行形成一个私有作用域
// 函数返回一个使用这个作用域中变量、属性的对象并被外面的变量占用
// 此时函数形成的私有作用域不会被销毁,即形成一个不销毁的栈内存
再给命名空间赋值的时候不是直接赋值一个对象,而是先执行匿名函数形成一个私有作用域 A(不销毁的栈内存),在 A 中创建一个堆内存并把堆内存的地址赋值给命名空间
限制一个类的实例化只能一次,一个类只有一个实例,并且提供一个访问它的全局访问点。
单例模式是创建型模式的一种。针对全局仅需一个对象的场景,比如全局缓存、全局状态管理、window 对象、线程池等等这些
class SingleTon {
constructor(name) {
this.name = name
this.instance = null
}
}
SingleTon.getInstance = function(name) {
if(!this.instance) {
this.instance = new SingleTon(name)
}
return this.instance
}
let s1 = SingleTon.getInstance('s1')
let s2 = SingleTon.getInstance('s2')
console.log(s1 === s2) // true
console.log(s1.name) // s1
console.log(s2.name) // s1
构造函数模式
- 主要用于组件、类库、插件、组件的封装
适配器模式
适配器用来解决两个接口不兼容的情况,不需要改变已有的接口,通过包装一层的方式实现两个接口的相互协作。
// 老接口
const oldCity = (function(){
return [{
name: 'beijing',
id: 11
}, {
name: 'baoding',
id: 12
}]
})()
// 新接口希望是下面形式
// {
// hangzhou: 11,
// jinhua: 12
// }
// 这时候就可采用适配者模式
const adaptor = function(oldCity) {
let obj = {}
for(let city of oldCity) {
obj[city.name] = city.id
}
return obj
}
console.log(adaptor(oldCity)) // {beijing: 11, baoding: 12}
装饰模式
装饰模式不需要改变已有的接口,作用是给对象添加功能。
var plan = {
fire: function() {
console.log('发射子弹')
}
}
plan.fire() // 发射子弹
var fire1 = plan.fire
var shot = function() {
console.log('发射散弹')
}
plan.fire = function() {
fire1()
shot()
}
plan.fire() // 发射子弹 发射散弹
var fire2 = plan.fire
var track = function() {
console.log('发射跟踪导弹')
}
plan.fire = function() {
fire2()
track()
}
plan.fire() // 发射子弹 发射散弹 发射跟踪导弹
代理模式
代理是为了控制对对象的访问,不让外部直接问访问到对象。
事件委托/代理 就用到了代理模式
示例如下:
- 小明送花
var Flower = function(num = '999') {
this.num = num
this.getNum = function() {
return `${this.num}朵`
}
}
var xiaoming = {
sendFlower: function(target) {
var flower = new Flower()
target.receiveFlower(flower)
}
}
var xiaohua = {
receiveFlower: function(flower) {
console.log(`receive flower ${flower.getNum()}`)
}
}
xiaoming.sendFlower(xiaohua) // receive flower 999朵
- 小明通过小红送花
var Flower = function(num = '999') {
this.num = num
this.getNum = function() {
return `${this.num}朵`
}
}
var xiaoming = {
sendFlower: function(target) {
var flower = new Flower()
target.receiveFlower(flower)
}
}
var xiaohong = {
receiveFlower: function(flower) {
return xiaohua.receiveFlower(flower)
}
}
var xiaohua = {
receiveFlower: function(flower) {
console.log(`receive flower ${flower.getNum()}`)
}
}
xiaoming.sendFlower(xiaohong) // receive flower 999朵
- 很显然,执行结果跟第一段代码一致,至此我们就完成了一个最简单的代理模式的编写。
- 我们会发现小明自己去送花和代理小红帮小明送花,二者看起来并没有本质的区别,引入一个代理对象看起来只是把事情搞复杂了而已。
- 的确,此处的代理模式毫无用处,它所做的只是把请求简单地转交给本体。
- 现在我们改变故事的背景设定,假设当小花在心情好的时候收到花,小明表白成功的几率有60%,而当小花在心情差的时候收到花,小明表白的成功率无限趋近于 0。
- 小明跟小花刚刚认识两天,还无法辨别她什么时候心情好。如果不合时宜地把花送给她,花被直接扔掉的可能性很大,这束花可是小明吃了 7 天泡面换来的。
- 但是小花的朋友小红却很了解她,所以小明只管把花交给小红,小红会监听小花的心情变化,然后选择她心情好的时候把花转交给她
代码如下:
var Flower = function(num = '999') {
this.num = num
this.getNum = function() {
return `${this.num}朵`
}
}
var xiaoming = {
sendFlower: function(target) {
var flower = new Flower()
target.receiveFlower(flower)
}
}
var xiaohong = {
receiveFlower: function(flower) {
xiaohua.listenGoodMood(function() { // 监听 xiaohua 的好心情
return xiaohua.receiveFlower(flower)
})
}
}
var xiaohua = {
receiveFlower: function(flower) {
console.log(`receive flower ${flower.getNum()}`)
},
listenGoodMood: function(fun) {
setTimeout(function() { // 假设 10 秒之后 xiaohua 的心情变好
fun()
}, 10000)
}
}
xiaoming.sendFlower(xiaohong) // receive flower 999朵
发布-订阅模式
也叫观察者模式,通过一对一或者一对多的关系,当对象发生改变时订阅双方都会收到通知。
在实际代码中其实发布-订阅模式也很常见,比如我们点击一个按钮触发了点击事件就是使用了该模式
发布-订阅模式
外观模式
外观模式提供了一个接口,隐藏了内部的逻辑,更加方便外部调用。
举个例子来说,我们现在需要实现一个兼容多种浏览器的添加事件方法
function addEvent(elm, evType, fn, useCapture) {
if (elm.addEventListener) {
elm.addEventListener(evType, fn, useCapture)
return true
} else if (elm.attachEvent) {
var r = elm.attachEvent("on" + evType, fn)
return r
} else {
elm["on" + evType] = fn
}
}
对于不同的浏览器,添加事件的方式可能会存在兼容问题。如果每次都需要去这样写一遍的话肯定是不能接受的,所以我们将这些判断逻辑统一封装在一个接口中,外部需要添加事件只需要调用 addEvent 即可。