前言
普通coder
写代码目的就是能跑功能能实现就行,而 大牛coder
写的代码会考虑性能、扩展、维护、兼容,设计模式在两者之间就像是一座阶梯,普通coder
深入理解设计模式并大量应用和练习也能一步步成为 大牛coder
。我们在接到需求时,不要马上动手,而要子弹飞一会~,停下来多思考怎么设计会更适合?
设计模式??听着很高大上,其实就是前人总结的写代码套路,而且套路多达二十多种,本文主要介绍 JS 语言中常用到的8种,有些套路我们可能有用过但对不上名字?,所以阅读时可能会有“哦,原来这个就是xx设计模式啊?”的感慨。为加深读者对套路的理解和记忆,文章结合了大量例子和经典的应用场景,客官请细细品尝。
这边先提一下,有一个代码原则很重要
【开放封闭】原则
什么是【开放封闭】?简单来说就是:当要改变或扩展程序时,可以增加代码,但不要修改原来的逻辑。
阅读文章时可以多留意下,很多设计模式是为了让代码遵循【开放封闭】原则。如果你写的代码遵守这个原则,那 bug 数量将会大大减少喔!
??下面开始套路时间..
1.代理模式
1.1对象代理
限制一个对象不能直接访问,需要第三者(代理)牵桥搭线才能访问,这就是代理模式。
通常使用 ES6 的 Proxy 来实现代理。
来看一个社交 App 的业务
- 用户未登录,仅看查看陌生人的头像、昵称等基本信息
- 用户为普通用户,可查看陌生人的基本信息外,还可查看学历
- 用户为Vip用户,除了以上信息还可查看陌生人的生活照片
// 要代理的陌生人
const stranger = {
nickname: 'jack ma',
portrait: '头像',
educationBackground: '本科',
lifePhotos: []
}
// 权限组
const baseInfo = ['nickname', 'portrait']
const loginInfo = ['educationBackground']
const vipInfo = ['lifePhotos']
// 登录用户
const user = {
isLogin: true,
isVIP: false,
}
// 实现代理
const objProxy = new Proxy(stranger, {
get(obj, key) {
if(!user.isLogin && loginInfo.indexOf(key) !== -1) {
console.log('请先登入')
return
}else if(!user.isVIP && vipInfo.indexOf(key) !== -1){
console.log('请先成为VIP')
return
}
return obj[key]
},
set(obj, key, val) {
return val
}
})
console.log(objProxy.nickname) // jack ma
console.log(objProxy.educationBackground) // 本科
console.log(objProxy.lifePhotos) // undefined
复制代码
1.2事件代理
当要给多个子元素添加点击事件时,开销比较大
<div id="father">
<a href="#">链接1号</a>
<a href="#">链接2号</a>
<a href="#">链接3号</a>
<a href="#">链接4号</a>
<a href="#">链接5号</a>
<a href="#">链接6号</a>
</div>
复制代码
我们可以将事件代理到父元素上
// 获取父元素
const father = document.getElementById('father')
// 给父元素安装一次监听函数
father.addEventListener('click', function(e) {
// 识别是否是目标子元素
if(e.target.tagName === 'A') {
// 以下是监听函数的函数体
e.preventDefault()
alert(`我是${e.target.innerText}`)
}
} )
复制代码
当点击子元素时,通过事件冒泡触发了父元素的点击方法,并通过 e.target
获取到真实被点击的子元素,这也属于一种代理模式。
1.3缓存代理
当有较复杂的运算时,我们可以通过缓存代理将已经计算过的值缓存起来,避免二次运算从而降低开销。
比如我们已实现一个计算所有入参之和的函数,我们可以通过代理将计算过的值缓存起来:
// 对传入的所有参数做求和
const addAll = function() {
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) {
// 如果有,则返回缓存池里现成的结果
return resultCache[args]
}
return resultCache[args] = addAll(...arguments)
}
})()
复制代码
以上例子中,resultCache 对象为缓存池,对象 key 为所有入参 arguments 转化的字符串,对象 value 为合计结果。这样一来,通过 proxyAddAll 就可以实现缓存代理。
2.单例模式
单例模式仅允许类或对象具有单个实例,并且它使用全局变量来存储该实例。
实现方法:判断是否存在该对象的实例,如果已存在则不再创建,如果不存在则创建实例并使用闭包将实例存起来
使用场景:适用于业务场景中只能存在一个的实例,比如弹窗,购物车
// 懒汉式
let ShopCar = (function () {
let instance;
function init() {
/*这里定义单例代码*/
return {
buy(good) {
this.goods.push(good);
},
goods: [],
};
}
return {
getInstance: function () {
if (!instance) {
instance = init();
}
return instance;
},
};
})();
let car1 = ShopCar.getInstance();
let car2 = ShopCar.getInstance();
car1.buy("橘子");
car2.buy("苹果");
console.log(car1.goods); //[ '橘子', '苹果' ]
console.log(car1 === car2); // true
复制代码
// 饿汉式
var ShopCar = (function () {
var instance = init();
function init() {
/*这里定义单例代码*/
return {
buy(good) {
this.goods.push(good);
},
goods: [],
};
}
return {
getInstance: function () {
return instance;
},
};
})();
let car1 = ShopCar.getInstance();
let car2 = ShopCar.getInstance();
car1.buy("橘子");
car2.buy("苹果"); //[ '橘子', '苹果' ]
console.log(car1.goods);
console.log(car1 === car2); // true
复制代码
实现效果有两种,懒汉式和饿汉式,各有优劣,按需求使用
- 懒汉式在类加载时,不创建实例,因此类加载速度快,但运行时获取对象的速度慢;
- 饿汉式在类加载时就完成了初始化,所以类加载较慢,但获取对象的速度快
3.策略模式
策略模式解难度不大,并且在面试中权重不高,所以能理解会使用即可。
策略模式简单来说就是利用对象映射,避免编写过多 if else
。
比如有这样一段代码:
// 询价方法,接受价格标签和原价为入参
function askPrice(tag, originPrice) {
// 处理预热价
if (tag === "pre") {
if (originPrice >= 100) {
return originPrice - 20;
}
return originPrice * 0.9;
}
// 处理大促价
if (tag === "onSale") {
if (originPrice >= 100) {
return originPrice - 30;
}
return originPrice * 0.8;
}
// 处理返场价
if (tag === "back") {
if (originPrice >= 200) {
return originPrice - 50;
}
return originPrice;
}
}
复制代码
使用策略模式改造后:
// 价格处理对象
let priceProcessor = {
prePrice(originPrice) {
if (originPrice >= 100) {
return originPrice - 20;
}
return originPrice * 0.9;
},
salePrice(originPrice) {
if (originPrice >= 100) {
return originPrice - 30;
}
return originPrice * 0.8;
},
backPrice(originPrice) {
if (originPrice >= 200) {
return originPrice - 50;
}
return originPrice;
},
};
// 询问价格
function askPrice(tag, originPrice) {
return priceProcessor[tag](originPrice);
}
复制代码
使用策略模式的优势:
- 不同逻辑之间隔离开,不会互相影响。比如当修改
prePrice
逻辑后仅需让测试同学回归该类型价格的功能即可 - 方便增加新的价格,直接
priceProcessor.newPrice
即可
综上可以总结出来策略模式的定义:定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。
用笔者自己的话来阐述:
将 if else 不同逻辑都独立到函数中,将这些函数映射到一个对象上,最后对外提供一个接口函数用于外部调用。
4.状态模式
状态模式(State Pattern) :允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。
class Fox {
constructor() {
this.animationType = "default";
this.foxLevel = 1;
}
// 动画处理器
animationProcessor = {
that: this,
default() {
console.log(`fox level 为 ${this.that.foxLevel} 的默认动画`);
},
feed() {
console.log(`fox level 为 ${this.that.foxLevel} 的喂食动画`);
},
touch() {
console.log(`fox level 为 ${this.that.foxLevel} 的抚摸动画`);
},
upLevel() {
console.log(`fox level 为 ${this.that.foxLevel} 的升级动画`);
},
// 喂食、升级组合动画
feedAndUpLevel() {
console.log(`fox level 为 ${this.that.foxLevel} 的喂食并且升级动画`);
},
};
// 触发动画
triggerAnimation(animationType) {
try {
this.animationProcessor[animationType]();
} catch (err) {
console.error("执行动画出错", err.message);
}
this.animationType = animationType;
}
}
const myFox = new Fox();
myFox.triggerAnimation("feed"); // fox level 为 1 的喂食动画
复制代码
5.工厂模式
什么时候用?
当类过多不方便管理,且需要创建的对象之间存在某些关联(有同一个父类、实现同一个接口等)时,不妨使用工厂模式
作用?
工厂模式提供一种集中化、统一化的方式,避免了分散创建对象导致的代码重复、灵活性差的问题。
5.1简单工厂模式
例如我们使用简单工厂模式:打造一个可制造多品牌汽车的工厂
// 汽车构造函数
function SuzukiCar(color) {
this.color = color;
this.brand = "Suzuki";
}
// 汽车构造函数
function HondaCar(color) {
this.color = color;
this.brand = "Honda";
}
// 汽车构造函数
function BMWCar(color) {
this.color = color;
this.brand = "BMW";
}
// 汽车品牌枚举
const BRANDS = {
suzuki: 1,
honda: 2,
bmw: 3,
};
/**
* 汽车工厂
*/
function CarFactory() {
this.create = function (brand, color) {
switch (brand) {
case BRANDS.suzuki:
return new SuzukiCar(color);
case BRANDS.honda:
return new HondaCar(color);
case BRANDS.bmw:
return new BMWCar(color);
default:
break;
}
};
}
复制代码
5.2抽象工厂模式
理解抽象工厂,要理清四个概念:
- 抽象工厂:抽象类,用于声明产品最终目标的共性。
- 具体工厂:继承自抽象工厂,实现了抽象工厂定义的方法,用于创建最终的产品。
- 抽象产品:抽象类,用于声明细粒度产品的共性。
- 具体产品:继承自抽象产品,实现了抽象产品定义的方法,用于创建细粒度的产品。具体工厂中接口的实现依赖于具体产品类。
咋一看概念晕乎乎的,来看个具体应用就清晰了:
// 抽象工厂
class MobileFactory {
constructor(name) {
this.brandName = name;
}
installOS() {
throw new Error("当前为抽象工厂,子品牌需实现该功能");
}
}
// 具体工厂
class HuaWei extends MobileFactory {
installOS(OS) {
OS.install(this);
}
}
// 抽象产品
class OS {
constructor(name) {
this.name = name;
}
install(phone) {
throw new Error("当前为抽象产品,需被继承并实现");
}
}
// 具体产品
class IosOS extends OS {
constructor() {
super("IOS");
}
install(phone) {
console.log(`${phone.brandName} install ${this.name} OS success`);
}
}
class AndroidOS extends OS {
constructor() {
super("Android");
}
install(phone) {
console.log(`${phone.brandName} install ${this.name} OS success`);
}
}
// 生产华为手机 p40
const huaweiP40 = new HuaWei("HuaWei P40");
const androidOSInstance = new AndroidOS();
huaweiP40.installOS(androidOSInstance); // HuaWei P40 install Android OS success
复制代码
从例子可以看出,抽象工厂
和 抽象产品
都是用来制定规范的,而真正干活的是 具体工厂
和 具体产品
。工厂和产品其实是类似的,只是粒度大小划分不同。
抽象工厂通过抽象工厂类和抽象产品类制定了规范,使得复杂业务变得清晰,最后具体工厂将具体产品拼接整合从而实现一个完整的产品。
6.装饰器模式
装饰器模式的原则:在不改动原来代码逻辑的基础上去添加增量代码。
假设已有旧代码:
window.onload = () => {
console.log(document.getElementByTagName("*").length); // 8
};
复制代码
现有需求,需在页面加载完成后打印 ‘页面加载完毕’ 。
使用装饰器模式来实现:
const fn = window.onload;
window.onload = () => {
typeof fn === "function" && fn();
console.log("页面加载完毕");
};
// 8
// 页面加载完毕
复制代码
使用装饰器模式,不会触碰到旧代码,大大降低了 bug 出现的几率。
7.适配器模式
适配器模式主要用来兼容代码。
假设有一个远古项目,数据请求使用的是 ajax 库。
// 发送get请求
Ajax('get', url地址, post入参, function(data){
// 成功的回调逻辑
}, function(error){
// 失败的回调逻辑
})
复制代码
现在需求要将数据请求库改为现代请求库 fetch 。
如果我们在调用了 Ajax 的地方都去修改,那修改、测试成本太高,这时候就可以用到适配器模式。
// Ajax适配器函数,入参与旧接口保持一致
async function AjaxAdapter(type, url, data, success, failed) {
const type = type.toUpperCase()
let result
try {
// 实际的请求全部由新接口发起
if(type === 'GET') {
result = await HttpUtils.get(url) || {}
} else if(type === 'POST') {
result = await HttpUtils.post(url, data) || {}
}
// 假设请求成功对应的状态码是1
result.statusCode === 1 && success ? success(result) : failed(result.statusCode)
} catch(error) {
// 捕捉网络错误
if(failed){
failed(error.statusCode);
}
}
}
// 用适配器适配旧的Ajax方法
async function Ajax(type, url, data, success, failed) {
await AjaxAdapter(type, url, data, success, failed)
}
复制代码
使用适配器模式:
- 要实现一个适配器让原先调用 Ajax 函数的参数能无缝衔接到新的请求库上
- 覆盖 Ajax 函数,调用适配器
这样一来,原本调用 Ajax 函数地方就可以不必需改直接复用。
8.观察者模式
观察者模式是面试的超高频考点。
观察者模式,又称发布订阅模式。发布者主要拥有:添加订阅,发布通知功能,订阅者主要有:接受通知,执行命令功能。
应用场景:
- Vue.js 的双向绑定
- Event Bus
观察模式的简单实现:
class Publisher {
constructor() {
this.observers = [];
}
addObserver(observer) {
this.observers.push(observer);
}
publish() {
this.observers.forEach((item) => {
item.receive();
});
}
}
class Observer {
receive() {
console.log("收到通知后执行命令");
}
}
const obs = new Observer();
const publish = new Publisher();
publish.addObserver(obs);
publish.publish(); // 收到通知后执行命令
复制代码
要进一步了解观察模式可以前往:
后语
最后祝大家在人际交往中,遇到的套路少一点,写代码时套路运用多一点,希望大家都能写出艺术品般的代码,而你就不是一个代码农民工???,而是一个艺术家!
其他文章