系列文章
- 使用JavaScript学习设计模式(1)| 小册免费学
- 使用JavaScript学习设计模式(2)| 小册免费学
- 使用JavaScript学习设计模式(3)| 小册免费学
- 使用JavaScript学习设计模式(4)| 小册免费学
结构型
装饰器模式
装饰器模式,又名装饰者模式。它的定义是“ 在不改变原对象的基础上,通过对其进行包装拓展,使原有对象可以满足用户的更复杂需求 ”。
装饰器案例
有一个弹窗函数,点击按钮后会弹出一个弹框。
function openModal() {
let div = document.craeteElement("div");
div.id = "modal";
div.innerHTML = "提示";
div.style.backgroundColor = "gray";
document.body.appendChlid(div);
}
btn.onclick = () => {
openModal();
};
但是忽然产品经理要改需求,要把提示文字由“提示”改为“警告”,背景颜色由 gray 改为 red。
听到这个你是不是立马就想直接改动源函数:
function openModal() {
let div = document.craeteElement("div");
div.id = "modal";
div.innerHTML = "警告";
div.style.backgroundColor = "red";
document.body.appendChlid(div);
}
但是如果是复杂的业务逻辑,或者这个代码时上任代码留下来的产物,在考虑到以后的需求变化,每次都这样修改确实很麻烦。
而且,直接修改已有的函数体,有违背了我们的“开放封闭原则”,往一个函数塞这么多的逻辑,也违背了“单一职责原则”,所以上面的方法并不是最佳的。
最省时省力的方式是不去关心它现有得了逻辑,只在此逻辑之上扩展新的功能即可,因此装饰器模式就此而生。
// 新逻辑
function changeModal() {
let div = document.getElemnetById("modal");
div.innerHTML = "告警";
div.style.backgroundColor = "red";
}
btn.onclick = () => {
openModal();
changeModal();
};
这种通过函数添加新的功能、而又不修改旧逻辑,这就是装饰器的魅力。
ES7 中的装饰器
在最新的 ES7 中有装饰器的提案,但是还未定案,所以语法可能不是最终版,但是思想是一样的。
- 装饰类的属性
@tableColor
class Table {
// ...
}
function tableColor(target) {
target.color = "red";
}
Table.color; // true
为Table这个类,添加一个tableColor的装饰器,即可改变Table的color属性
- 装饰类的方法
class Person {
@readonly
name() {
return `${this.first} ${this.last}`;
}
}
为Person类的name方法添加只读的装饰器,使得该方法不可被修改。
其实是借助Object.defineProperty的wirteable特性实现的。
-
装饰函数
因为 JS 中函数存在函数提升,直接使用装饰器并不可取,但是可以使用高级函数的方式实现。
function doSomething(name) { console.log("Hello, " + name); } function loggingDecorator(wrapped) { return function() { console.log("fun-Starting"); const result = wrapped.apply(this, arguments); console.log("fun-Finished"); return result; }; } const wrapped = loggingDecorator(doSomething); let name = "World"; doSomething(name); // 装饰前 // output: // Hello, World wrapped(name); // 装饰后 // output: // fun-Starting // Hello, World // fun-Finished上面的装饰器,是给一个函数在执行开始和执行结束分别打印一个 log。
参考
适配器模式
适配器模式的作用是解决两个软件实体间的接口不兼容问题。使用适配器模式之后,原本由于接口不兼容而不能工作的两个软件实体可以一起工作。
简单来说,就是把一个类的接口变成客户端期待的另一种接口,解决兼容问题。
比如:axios
例子:一个渲染地图的方法,默认是调用当前地图对象的 show 方法进行渲染操作,当有多个地图,而每个地图的渲染方法都不一样时,为了方便使用者调用,就需要做适配了。
let googleMap = {
show: () => {
console.log("开始渲染谷歌地图");
},
};
let baiduMap = {
display: () => {
console.log("开始渲染百度地图");
},
};
let baiduMapAdapter = {
show: () => {
return baiduMap.display();
},
};
function renderMap(obj) {
obj.show();
}
renderMap(googleMap); // 开始渲染谷歌地图
renderMap(baiduMapAdapter); // 开始渲染百度地图
这其中对“百度地图”做了适配的处理。
小结
- 适配器模式主要解决两个接口之间不匹配的问题,不会改变原有的接口,而是由一个对象对另一个对象的包装
- 适配器模式符合开放封闭原则
- 把变化留给自己,把统一留给用户。
代理模式
代理模式——在某些情况下,出于种种考虑/限制,一个对象不能直接访问另一个对象,需要一个第三者(代理)牵桥搭线从而间接达到访问目的,这样的模式就是代理模式。
提起代理(Proxy),对于前端很熟悉的,我能联想到一系列的东西,比如:
- ES6 新增的 proxy 属性
- 为了解决跨域问题而经常使用的 webpack 的 proxy 配置和 Nginx 代理
- 还有科学上网所使用的的代理。
- 等等
事件代理
常见的列表、表格都需要单独处理事件时,使用父级元素事件代理,可以极大的减少代码量。
<div id="father">
<span id="1">新闻1</span>
<span id="2">新闻2</span>
<span id="3">新闻3</span>
<span id="4">新闻4</span>
<span id="5">新闻5</span>
<span id="6">新闻6</span>
<!-- 7、8... -->
</div>
如上代码,我想点击每个新闻,都可以拿到当前新闻的id,从而进行下一步操作。
如果给每一个span都绑定一个onclick事件,就太耗费性能了,而且写起来也很麻烦。
我们常见的做法是利用事件冒泡的原理,将事件带代理到父元素上,然后统一处理。
let father = document.getElementById("father");
father.addEventListener("click", (evnet) => {
if (event.target.nodeName === "SPAN") {
event.preventDefault();
let id = event.target.id;
console.log(id); // 拿到id,进行下一步操作
}
});
虚拟代理
例如:某个花销很大的操作,可以通过虚拟代理的方式延迟到这种需要它的时候才去创建(例如:使用虚拟代理实现图片懒加载)
图片预加载:先通过一张 loading 图占位,然后通过异步的方式加载图片,等图片加载完成之后在使用原图替换 loading 图。
问什么要使用预加载+懒加载?以淘宝举例,商城物品图片多之又多,一次全部请求过来这么多图片无论是对 js 引擎还是浏览器本身都是一个巨大的工作量,会拖慢浏览器响应速度,用户体验极差,而预加载+懒加载的方式会大大节省浏览器请求速度,通过预加载率先加载占位图片(第二次及以后都是缓存中读取),再通过懒加载直到要加载的真实图片加载完成,瞬间替换。这种模式很好的解决了图片一点点展现在页面上用户体验差的弊端。
须知:图片第一次设置 src,浏览器发送网络请求;如果设置一个请求过的 src 那么浏览器则会从缓存中读取 from disk cache
class PreLoadImage {
constructor(imgNode) {
// 获取真实的DOM节点
this.imgNode = imgNode;
}
// 操作img节点的src属性
setSrc(imgUrl) {
this.imgNode.src = imgUrl;
}
}
class ProxyImage {
// 占位图的url地址
static LOADING_URL = "xxxxxx";
constructor(targetImage) {
// 目标Image,即PreLoadImage实例
this.targetImage = targetImage;
}
// 该方法主要操作虚拟Image,完成加载
setSrc(targetUrl) {
// 真实img节点初始化时展示的是一个占位图
this.targetImage.setSrc(ProxyImage.LOADING_URL);
// 创建一个帮我们加载图片的虚拟Image实例
const virtualImage = new Image();
// 监听目标图片加载的情况,完成时再将DOM上的真实img节点的src属性设置为目标图片的url
virtualImage.onload = () => {
this.targetImage.setSrc(targetUrl);
};
// 设置src属性,虚拟Image实例开始加载图片
virtualImage.src = targetUrl;
}
}
ProxyImage 帮我们调度了预加载相关的工作,我们可以通过 ProxyImage 这个代理,实现对真实 img 节点的间接访问,并得到我们想要的效果。
在这个实例中,virtualImage 这个对象是一个“幕后英雄”,它始终存在于 JavaScript 世界中、代替真实 DOM 发起了图片加载请求、完成了图片加载工作,却从未在渲染层面抛头露面。因此这种模式被称为“虚拟代理”模式。
【点击查看Demo】:虚拟代理-在线例子
缓存代理
缓存代理比较好理解,它应用于一些计算量较大的场景里。在这种场景下,我们需要“用空间换时间”——当我们需要用到某个已经计算过的值的时候,不想再耗时进行二次计算,而是希望能从内存里去取出现成的计算结果。
这种场景下,就需要一个代理来帮我们在进行计算的同时,进行计算结果的缓存了。
例子:对参数求和函数进行缓存代理。
// addAll方法会对你传入的所有参数做求和操作
const addAll = function() {
console.log("进行了一次新计算");
let result = 0;
const len = arguments.length;
for (let i = 0; i < len; i++) {
result += arguments[i];
}
return result;
};
// 为求和方法创建代理
const proxyAddAll = (function() {
// 求和结果的缓存池
const resultCache = {};
return function() {
// 将入参转化为一个唯一的入参字符串
const args = Array.prototype.join.call(arguments, ",");
// 检查本次入参是否有对应的计算结果
if (args in resultCache) {
// 如果有,则返回缓存池里现成的结果
console.log("无计算-使用缓存的数据");
return resultCache[args];
}
return (resultCache[args] = addAll(...arguments));
};
})();
let sum1 = proxyAddAll(1, 2, 3); // 进行了一次新计算
let sum2 = proxyAddAll(1, 2, 3); // 无计算-使用缓存的数据
第一次进行计算返回结果,并存入缓存。如果再次传入相同的参数,则不计算,直接返回缓存中存在的结果。
在常见在 HTTP 缓存中,浏览器就相当于进行了一层代理缓存,通过 HTTP 的缓存机制控制(强缓存和协商缓存)判断是否启用缓存。
频繁却变化小的的网络请求,比如getUserInfo,可以使用代理请求,设置统一发送和存取。
小结
- 代理模式符合开放封闭原则。
- 本体对象和代理对象拥有相同的方法,在用户看来并不知道请求的是本体对象还是代理对象。
桥接模式
桥接模式:将抽象部分和具体实现部分分离,两者可独立变化,也可以一起工作。
在这种模式的实现上,需要一个对象担任“桥”的角色,起到连接的作用。
例子:
JavaScript 中桥接模式的典型应用是:Array对象上的forEach函数。
此函数负责循环遍历数组每个元素,是抽象部分; 而回调函数callback就是具体实现部分。
下方是模拟forEach方法:
const forEach = (arr, callback) => {
if (!Array.isArray(arr)) return;
const length = arr.length;
for (let i = 0; i < length; ++i) {
callback(arr[i], i);
}
};
// 以下是测试代码
let arr = ["a", "b"];
forEach(arr, (el, index) => console.log("元素是", el, "位于", index));
// 元素是 a 位于 0
// 元素是 b 位于 1
外观模式
外观模式(Facade Pattern)隐藏系统的复杂性,并向客户端提供了一个客户端可以访问系统的接口。这种类型的设计模式属于结构型模式,它向现有的系统添加一个接口,来隐藏系统的复杂性。
这种模式涉及到一个单一的类,该类提供了客户端请求的简化方法和对现有系统类方法的委托调用。
例子
外观模式即执行一个方法可以让多个方法一起被调用。
涉及到兼容性,参数支持多个格式、环境等等.. 对外暴露统一的 api
比如自己封装的事件对象包含了阻止冒泡和添加事件监听的兼容方法:
const myEvent = {
stop (e){
if(typeof e.preventDefault() == 'function'){
e.preventDefault();
}
if(typeof e.stopPropagation() == 'function'){
e.stopPropagation()
}
// IE
if(typeOd e.retrunValue === 'boolean'){
e.returnValue = false
}
if(typeOd e.cancelBubble === 'boolean'){
e.returnValue = true
}
}
addEvnet(dom, type, fn){
if(dom.addEventListener){
dom.addEventlistener(type, fn, false);
}else if(dom.attachEvent){
dom.attachEvent('on'+type, fn)
}else{
dom['on'+type] = fn
}
}
}
组合模式
组合模式(Composite Pattern),又叫部分整体模式,是用于把一组相似的对象当作一个单一的对象。
组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。
这种模式创建了一个包含自己对象组的类。该类提供了修改相同对象组的方式。
例子
想象我们现在手上有多个万能遥控器,当我们回到家中,按一下开关,下列事情将被执行
- 开门
- 开电脑
- 开音乐
// 先准备一些需要批量执行的功能
class GoHome {
init() {
console.log("开门");
}
}
class OpenComputer {
init() {
console.log("开电脑");
}
}
class OpenMusic {
init() {
console.log("开音乐");
}
}
// 组合器,用来组合功能
class Comb {
constructor() {
// 准备容器,用来防止将来组合起来的功能
this.skills = [];
}
// 用来组合的功能,接收要组合的对象
add(task) {
// 向容器中填入,将来准备批量使用的对象
this.skills.push(task);
}
// 用来批量执行的功能
action() {
// 拿到容器中所有的对象,才能批量执行
this.skills.forEach((val) => {
val.init();
});
}
}
// 创建一个组合器
let c = new Comb();
// 提前将,将来要批量操作的对象,组合起来
c.add(new GoHome()); // 添加'开门'命令
c.add(new OpenComputer()); // 添加'开电脑'命令
c.add(new OpenMusic()); // 添加'开音乐'命令
c.action(); // 执行添加的所有命令
小结
- 组合模式在对象间形成
树形结构 - 组合模式中对基本对象和组合对象
被一致对待 - 无需关心对象有多少层,调用时只需要在
根部进行调用 - 将多个对象的功能,组装起来,实现
批量执行
享元模式
享元模式(Flyweight Pattern)主要用于减少创建对象的数量,以减少内存占用和提高性能。
这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结构的方式。
特点
- 共享内存(主要是考虑内存,而非效率)
- 相同的数据(内存),共享使用
例子
比如常见的事件代理,通过将若干个子元素的事件代理到一个父元素,子元素共同使用一个方法。如果都绑定到<span>标签,对内存开销太大 。
<!-- 点击span,拿到当前的span中的内容 -->
<div id="box">
<span>1</span>
<span>2</span>
<span>3</span>
<span>4</span>
</div>
<script>
var box = document.getElementById("box");
box.addEventListener("click", function(e) {
let target = e.target;
if (e.nodeName === "SPAN") {
alert(target.innerHTML);
}
});
</script>
小结
- 将相同的部分抽象出来
- 符合开放封闭的原则
来自九旬的原创:博客原文链接
本文正在参与「掘金小册免费学啦!」活动, 点击查看活动详情