创建型模式
工厂模式
工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象,用工厂方法代替new操作的一种模式。
class Creator {
create(name) {
return new Animal(name)
}
}
class Animal {
constructor(name) {
this.name = name
}
}
var creator = new Creator()
var duck = creator.create('Duck')
console.log(duck.name) // Duck
var chicken = creator.create('Chicken')
console.log(chicken.name) // Chicken
复制代码
小结:
- 构造函数和创建者分离,对new操作进行封装
- 符合开放封闭原则
单例模式
单例模式的定义是, 保证一个类仅有一个实例, 并提供一个访问他的全局方法。
单例模式是一种常用的模式, 有一些对象我们往往只需要一个, 比如线程池、全局缓存、浏览器中的window对象等。 在JavaScript开发中, 单例模式的用途同样非常广泛。试想一下,当我们单击登录按钮的时候,页面中会出现一个登录浮窗,而这个登录浮窗是唯一的,无论单击多少次登录按钮, 这个浮窗只会被创建一次, name这个登录浮窗就适合单例模式来闯将。
举一个登录框的例子,代码如下:
<!DOCTYPE html>
<html lang="en">
<body>
<button id="btn">登录</button>
</body>
<script>
class Login {
createLayout() {
var oDiv = document.createElement('div')
oDiv.innerHTML = '我是登录框'
document.body.appendChild(oDiv)
oDiv.style.display = 'none'
return oDiv
}
}
class Single {
getSingle(fn) {
var result;
return function() {
return result || (result = fn.apply(this, arguments))
}
}
}
var oBtn = document.getElementById('btn')
var single = new Single()
var login = new Login()
// 由于闭包,createLoginLayer对result的引用,所以当single.getSingle函数执行完之后,内存中并不会销毁result。
// 当第二次以后点击按钮,根据createLoginLayer函数的作用域链中已经包含了result,所以直接返回result
// 将获取单例和创建登录框的方法解耦,符合开放封闭原则
var createLoginLayer = single.getSingle(login.createLayout)
oBtn.onclick = function() {
var layout = createLoginLayer()
layout.style.display = 'block'
}
</script>
</html>
复制代码
小结:
1.单例模式的主要思想就是,实例如果已经创建,则直接返回。
function createSingleton() {
let obj = null
// 实例如果已经创建过,则直接返回
if (!obj) {
obj = xxx
}
return obj
}
复制代码
2.符合开放封闭原则
结构型模式
适配器模式
适配器模式的作用是解决两个软件实体间的接口不兼容的问题。使用适配器模式后,原来由于接口不兼容而不能工作的两个软件实体可以一起工作。
适配器的别名是包装器(wrapper),这是一个相对简单的模式。在程序开发中有许多这样的场景:当我们试图用模块或对象的某个接口时,却发现这个接口的格式并不符合目前的需求。这时候有两种解决方法,第一种是修改原来的接口实现,但是原来的模块很复杂,或者我们拿到的模块是一段别人编写的经过压缩的代码,修改原接口就显得不太现实了。第二种方法是创建一个适配器,将原接口转换为客户希望的另一个接口,客户只需要和适配器打交道。
举一个渲染地图的例子:
class GoogleMap {
show() {
console.info('渲染谷歌地图')
}
}
class BaiduMap {
show() {
console.info('渲染百度地图')
}
}
function render(map) {
if (map.show instanceof Function) {
map.show()
}
}
render(new GoogleMap()) // 渲染谷歌地图
render(new BaiduMap()) // 渲染百度地图
复制代码
但是假如BaiduMap类的原型方法不叫show,而是叫display,这时候就可以使用适配器模式了,因为我们不能轻易的改变第三方的内容。在BaiduMap的基础上封装一层,对外暴露show方法。
class GooleMap {
show() {
console.log('渲染谷歌地图')
}
}
class BaiduMap {
display() {
console.log('渲染百度地图')
}
}
// 定义适配器类, 对BaiduMap类进行封装
class BaiduMapAdapter {
show() {
const baiduMap = new BaiduMap()
return baiduMap.display()
}
}
function render(map) {
if (map.show instanceof Function) {
map.show()
}
}
render(new GooleMap()) // 渲染谷歌地图
render(new BaiduMapAdapter()) // 渲染百度地图
复制代码
小结:
- 适配器模式主要解决两个接口之间不匹配的问题,不会改变原有的接口,而是由一个对象对另一个对象的包装。
- 适配器模式符合开放封闭原则
代理模式
代理模式是为一个对象提供一个代用品或占位符,以便控制对它的访问。
代理模式是一种非常有意义的模式,在生活中可以找到很多代理模式的场景。比如,明星都有经纪人作为代理。如果想请明星来办一场商业演出,只能联系他的经纪人。经纪人会把商业演出的细节和报酬都谈好后,再把合同交给明星签。
本文举一个使用代理对象加载图片的例子来理解代理模式,当网络不好的时候,图片的加载需要一段时间,这就会产生空白,影响用户体验,这时候我们可在图片真正加载完之前,使用一张loading占位图片,等图片真正加载完再给图片设置src属性。
class MyImage {
constructor() {
this.img = new Image()
document.body.appendChild(this.img)
}
setSrc(src) {
this.img.src = src
}
}
class ProxyImage {
constructor() {
this.proxyImage = new Image()
}
setSrc(src) {
let myImageObj = new MyImage()
myImageObj.img.src = 'loading.png'
this.proxyImage.src = src
this.proxyImage.onload = function() {
myImageObj.img.src = src
}
}
}
const proxyImage = new ProxyImage()
proxyImage.setSrc('pic.png')
复制代码
本例中,本体类中有自己的setSrc方法,如果有一天网络速度已经不需要预加载了,我们可以直接使用本体对象的setSrc方法,,并且不需要改动本体类的代码,而且可以删除代理类。
// 依旧可以满足需求
var myImage = new MyImage()
myImage.setSrc('https://images2.alphacoders.com/103/thumbbig-1031920.webp')
复制代码
小结:
- 代理模式符合开放封闭原则
- 本体对象和代理对象拥有相同的方法,在用户看来并不知道请求的本体对象还是代理对象。
行为型模式
策略模式
定义一系列的算法,把它们一个个封装起来,并使它们可以替换
var fnA = function(val) {
return val * 1
}
var fnB = function(val) {
return val * 2
}
var fnC = function (val) {
return val * 3
}
var calculate = function(fn, val) {
return fn(val)
}
console.log(calculate(fnA, 100))// 100
console.log(calculate(fnB, 100))// 200
console.log(calculate(fnC, 100))// 300
复制代码
观察者模式(订阅-发布模式)
发布-订阅模式又叫观察者模式,它定义对象间一种一对多的依赖关系,当一个对象的状态发生变化时, 所有依赖它的对象都将得到通知。
先实现一个简单的发布-订阅模式,代码如下:
class Event {
constructor() {
this.eventTypeObj = {}
}
on(eventType, fn) {
if (!this.eventTypeObj[eventType]) {
// 按照不同的订阅事件类型, 储存不同的订阅回调
this.eventTypeObj[eventType] = []
}
this.eventTypeObj[eventType].push(fn)
}
emit() {
// 可以理解为arguments借用shift方法
let eventType = Array.prototype.shift.call(arguments)
let eventList = this.eventTypeObj[eventType]
for (let i = 0; i < eventList.length; i++) {
eventList[i].apply(eventList[i], arguments)
}
}
remove(eventType, fn) {
// 如果使用remove方法, fn为函数名称,不能是匿名函数
let eventTypeList = this.eventTypeObj[eventType]
if (!eventTypeList) {
// 如果没有被人订阅过事件, 直接返回
return false
}
if (!fn) {
// 如果没有传入取消订阅的回调函数,则该订阅类型的事件全部取消
eventTypeList && (eventTypeList.length = 0)
} else {
for (let i = 0; i < eventTypeList.length; i++) {
if (eventTypeList[i] === fn) {
eventTypeList.splice(i, 1)
// 删除之后, i--保证下轮循环不会漏掉没有被遍历到的函数名
i--
}
}
}
}
}
var handleFn = function(data) {
console.log(data)
}
var event = new Event()
event.on('click', handleFn)
event.emit('click', '1') // 1
event.remove('click', handleFn)
event.emit('click', '2') // 不打印
复制代码
以上代码可以满足先订阅后发布,但是如果先发布消息,后订阅就不满足了。这时候我们可以稍微修改一下即可满足先发布后订阅,在发布消息时,把事件缓存起来,等有订阅者时再执行。代码如下:
class Event {
constructor() {
this.eventTypeObj = {}
this.cacheObj = {}
}
on(eventType, fn) {
if (!this.eventTypeObj[eventType]) {
// 按照不同的订阅事件类型,存储不同的订阅回调
this.eventTypeObj[eventType] = []
}
this.eventTypeObj[eventType].push(fn)
// 如果是先发布,则在订阅者订阅后,则根据发布后缓存的事件类型和参数,执行订阅者的回调
if (this.cacheObj[eventType]) {
var cacheList = this.cacheObj[eventType]
for (var i = 0; i < cacheList.length; i++) {
cacheList[i]()
}
}
}
emit() {
// 可以理解为arguments借用shift方法
var eventType = Array.prototype.shift.call(arguments)
var args = arguments
var that = this
function cache() {
if (that.eventTypeObj[eventType]) {
var eventList = that.eventTypeObj[eventType]
for (var i = 0; i < eventList.length; i++) {
eventList[i].apply(eventList[i], args)
}
}
}
if (!this.cacheObj[eventType]) {
this.cacheObj[eventType] = []
}
// 如果先订阅,则直接订阅后发布
cache(args)
// 如果先发布后订阅,则把发布的事件类型与参数保存起来,等到有订阅后执行订阅
this.cacheObj[eventType].push(cache)
}
}
复制代码
小结:
- 发布订阅模式可以使代码解耦,满足开放封闭原则
- 当过多的使用发布订阅模式,如果订阅消息始终都没有触发,则订阅者一直保存在内存中。
命令模式
在软件系统中, 行为请求者与行为实现者通常呈现一种紧耦合。但在某些场合, 比如要对行为进行记录、撤销/重做、事务等处理,这种无法抵御变化的紧耦合是不合适的。在这种情况下, 如何将行为请求者与行为实现者解耦?将一组行为抽象为对象,实现二者之间的松耦合。这就是命令模式(Command Pattern)。
在命令的发布者和接收者之间,定义一个命令对象,命令对象暴露出一个统一的接口给命令的发布者,而命令的发布者不用去管接收者是如何执行命令的,做到命令发布者和接收者的解耦。
举一个如果页面中有3个按钮,给不同按钮添加不同功能的例子,代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>cmd-demo</title>
</head>
<body>
<div>
<button id="btn1">按钮1</button>
<button id="btn2">按钮2</button>
<button id="btn3">按钮3</button>
</div>
<script>
var btn1 = document.getElementById('btn1')
var btn2 = document.getElementById('btn2')
var btn3 = document.getElementById('btn3')
// 定义一个命令发布者的类
class Executor {
setCommand(btn, command) {
btn.onclick = function() {
command.execute()
}
}
}
// 定义一个命令接收者
class Menu {
refresh() {
console.info('刷新菜单')
}
addSubMenu() {
console.info('添加子菜单')
}
}
// 定义一个刷新菜单的命令对象的类
class RefreshMenu {
constructor(receiver) {
// 命令对象与接收者关联
this.receiver = receiver
}
// 暴露出统一的接口给命令发布者Executor
execute() {
this.receiver.refresh()
}
}
// 定义一个添加子菜单的命令对象的类
class AddSubMenu {
constructor(receiver) {
// 命令对象的接收者关联
this.receiver = receiver
}
// 暴露出统一的接口给命令发布者Executor
execute() {
this.receiver.addSubMenu()
}
}
var menu = new Menu()
var executor = new Executor()
var refreshMenu = new RefreshMenu(menu)
// 给按钮1添加刷新功能
executor.setCommand(btn1, refreshMenu)
var addSubMenu = new AddSubMenu(menu)
// 给按钮2添加增加子菜单功能
executor.setCommand(btn2, addSubMenu)
// 如果想给按钮3增加删除菜单的功能,就继续增加删除菜单的命令对象和接收者的具体删除方法,而不必修改命令对象
</script>
</body>
</html>
复制代码