《JavaScript设计模式》----张荣铭(二)
首先说一下什么是设计模式?以及我们为什么要学习设计模式?
设计模式的定义是:设计模式是在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案
也可以通俗的理解为:设计模式在某个特定场景下都某种问题的解决方案。当然,这也就是为什么我们要学习设计模式的原因。本书将设计模式按照类型分成六大类
- 创建型设计模式
- 结构型设计模式
- 行为型设计模式
- 技巧型设计模式
- 架构型设计模式
2 结构型设计模式
2.1 外观模式
外观模式就是将一些底层的操作封装起来,很多代码库就是做了这样一些工作,比如工具库lodash,可视化的d3
举个简单的例子
var A = {
g: function(id) {
return document.getElementById(id);
},
css: function(id, key, value) {
var ele = document.getElementById('id')
ele.style[key] = value;
return ele;
},
attr: function(id, key, value) {
var ele = document.getElementById(id);
ele[key] = value;
},
on: function(id, type, fn) {
var ele = document.getElementById(id);
ele[`on${type}`] = fn;
}
}
A.css('box', backgroud, 'red');
A.attr('box', 'className', 'box');
A.on('box', 'click', () => {console.log('click event!');})
// 链式调用
A.css('box', backgroud, 'red')
.attr('box', 'className', 'box')
.on('box', 'click', () => {console.log('click event!');})
2.2 适配器模式
将一个类(对象)的接口(方法)转化成另外一个接口以满足用户需求。
比如一个接口,现在既要对App端做兼容又要对h5端做兼容,那么就对接口进行改写,以满足多处需求,这就是适配器模式。
又比如原来的函数需要在多个地方同时使用,但是在使用的过程中参数个数又是不一样的,那么就需要对这个函数进行改写,以满足多处同时使用。
2.3 代理模式
代理模式是为一个对象提供一个代用品或者占位符,以便控制对它的访问
Proxy也可以理解成,在目标对象之前架设一层’拦截’,外界对该对象的访问,都必须先通过这层拦截
-
像我们单例模式中,我们去创建一个对象不是直接创建,而是通过一个中间人,先判断一些是否存在这个实例,如果有直接返回已存在
-
虚拟代理模式:某一个开销很大的操作,可以通过虚拟代理的方式延迟到这种需要它的时候才去创建(例: 使用虚拟代理懒加载图片)
// **下面这个例子感觉没有很清晰**
var imgFunc = (() => {
var imgNode = document.createElement('img');
document.body.appendChild(imgNode);
return {
setSrc: (src) => {
imgNode.src = src;
}
}
})();
var proxyImage = (() => {
var img = new Image();
// 这里的this指向img,this永远指向最后调用他的那个对象
img.onload = function () {
imgFunc.setSrc(this.src);
}
return {
setSrc: (src) => {
// 这个例子的核心就是,我们不直接加载图片,而是先再代理对象中加载loading图片,然后给img设置Image,当图片加载完成的时候,再调用真实对象的setSrc
imgFunc.setSrc('./loading.png');
setTimeout(() => {
img.src = src;
}, 2000)
}
}
})();
proxyImage.setSrc('./home.png');
2.4 装饰者模式
装饰者模式:在不改变原来对象的基础上,通过对其进行包装拓展(可插拔)
之前做过一个简单的实验,要求一个功能的增删改查之前先要登录,那么就可以在增删改查之前新增一个login校验的装饰器
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
#button1 {
background: #eee;
width: 100px;
height: 50px;
}
#input1 {
background: red;
width: 100px;
height: 50px;
}
#div1 {
border: 1px solid blue;
width: 100px;
height: 50px;
}
</style>
</head>
<body>
<button id="button1">这是一个button</button>
<span id="input1">这是一个input</span>
<div id="div1">这是一个div</div>
</body>
<script>
const button = document.getElementById('button1');
const input = document.getElementById('input1');
const div = document.getElementById('div1');
[button, input, div].forEach((ele) => {
ele.onclick = () => {
console.log(`${ele.innerHTML} + 被点击了!`);
}
})
const decorator = (ele, fn) => {
if (typeof ele.onclick === 'function') {
const stagedEvent = ele.onclick;
ele.onclick = () => {
// 老的事件
stagedEvent();
// 新增的事件
fn();
}
} else {
ele.onclick();
}
}
[button, input, div].forEach((ele) => {
decorator(ele, () => {
console.log('我被加了一个装饰器!');
})
})
</script>
</html>
2.5 桥接模式
在系统沿着多个维度变化的同时,又不增加其复杂度并已达到解耦。(先抽象提取公共部分,然后将实现与公共部分通过桥接函数方法链接在一起)
- 第一步:提取公共部分
- 第二步:实现
- 第三步:通过桥接函数将实现与抽象链接在一起,达到解耦的目的
比如我们来看下面的例子:
var divs = document.getElementByTagName('div');
var numDiv = document.getElementById('num');
div[0].mouseover = () => {
this.style.color = 'red';
this.style.background = '#ddd';
}
div[0].mouseover = () => {
this.style.color = '#333';
this.style.background = '#f5f5f5';
}
div[1].mouseover = () => {
numDiv.style.color = 'red';
numDiv.style.background = '#ddd';
}
div[1].mouseout = () => {
numDiv.style.color = '#333';
numDiv.style.background = '#f5f5f5';
}
很显然 我们事件的回调函数是可以做抽象的
const changeColor = (ele, color, bgColor) => {
ele.style.color = color;
ele.style.background = baColor;
}
然后作为事件回调的一部分
div[0].mouseover = () => {
// ... 相当于和事件的回调解耦 还可以添加其他的 桥接函数 改变颜色的功能只需要在
// changeColor里修改就可以
changeColor(this, 'red', '#ddd');
}
div[0].mouseout = () => {
// ... 添加其他维度的事件 但是对changeColor没有任何影响
changeColor(this, '#333', '#f5f5f5');
}
2.6 组合模式
组合模式又称部分-整体模式,将对象组合成树形结构以表示部分整体
的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。
举个例子,中午吃午饭的时候我们不需要饮料,肉,青菜一个个去点,餐厅直接提供套餐,我们直接点套餐就可以享受美味。而对于厨师来讲,他们又是专注于每一道菜的。
再举一个例子:
// 新建一个关门的命令
var closeDoorCommand = {
execute: function(){
console.log( '关门' );
}
};
// 新建一个开电脑的命令
var openPcCommand = {
execute: function(){
console.log( '开电脑' );
}
};
// 登陆QQ的命令
var openQQCommand = {
execute: function(){
console.log( '登录QQ' );
}
};
// 创建一个宏命令
var MacroCommand = function(){
return {
// 宏命令的子命令列表
commandsList: [],
// 添加命令到子命令列表
add: function( command ){
this.commandsList.push( command );
},
// 依次执行子命令列表里面的命令
execute: function(){
for ( var i = 0, command; command = this.commandsList[ i++ ]; ){
command.execute();
}
}
}
};
var macroCommand = MacroCommand();
macroCommand.add( closeDoorCommand );
macroCommand.add( openPcCommand );
macroCommand.add( openQQCommand );
macroCommand.execute();
组合模式将对象组合成树形结构,以表示“部分-整体”的层次结构。除了用来表示树形结构之外,组合模式的另一个好处是通过对象的多态性表现,使得用户对单个对象和组合对象的使用具有一致性(nice)。另外,客户端使用可以与复合结构复杂的情况相解藕(其实和前面一个意思)
2.6 城市公交车-享元模式
运用共享技术有效地支持大量的细粒度的对象,避免对象间拥有相同内容造成多余的开销
比如有这样一个需求:一个新闻翻页功能,点击下一页隐藏当前页面的新闻,然后显示后面的5个,那么为了避免不要的DOM开销,我们可以每次都使用创建好的5个DOM节点,而不是每次都去创建一个新的节点(其实目前很多框架的思想都是这样的,比如React和Vue中我们只是数据的改变,然后重新render下视图,就能实现这个需求)