前言
作为前端开发,如果是想要提升自己能力和技术水平,不能只是简单的重复造轮子,必须要深刻理解体会前端的设计模式,有助于自身能力的提升。
什么是前端设计模式
所谓前端的设计模式就是一种可以在多处地方重复使用的代码方案设计,只是不同的设计模式所应用的场景也有所不同。
前端设计模式分类
详细来树洞额话设计模式高达二十多种,本文章主要针对于javascript相关的设计模式,针对于其中的10种模式进行分类总结和介绍。 javascript的设计模式总共分为三大类型:创建型、结构性和行为型
-
创建型:单例模式、工厂模式、适配器模式等。
-
结构型:适配器模式、装饰器模式、代理模式等。
-
行为性:观察者模式、发布订阅模式、命令模式、模板模式等。
前端设计模式-创建型
创建型:用于创建的过程。通过确定规则对代码进行封装,减少创建过程中的重复代码,并且对创建制定规则提高规范和灵活性。
-
单例模式
特点:确保一个类只有一个实例,并且提供一个访问它全局的访问点。
优点:由于只有一个实例,所以全局唯一性,并且可以更好地控制共享资源优化性能。
示例:const test = { name: '小明', age: '18', }; export default test; import test from './test'; console.log(test.name,test.age); // 打印:小明,18
上述例子定义test并且export default 暴露唯一的实例 test,符合确保一个类只有一个实例,并且提供了一个访问它的全局访问点原则。
-
工厂模式
特点:对代码逻辑进行封装,只暴露出通用的接口直接调用。
优点:对逻辑进行高度封装,降低耦合度,易于维护代码和提高后续扩展性。
示例:// ------ 定义一个类 ------ class testProduct { constructor(productName) { this.productName = productName; } getName() { console.log(`名称: ${this.productName}`); } } // ----- 定义一个工厂函数 ------- function createProduct(name) { return new testProduct(name); } // 使用工厂函数创建对象 const test1 = createProduct('test1'); const test2 = createProduct('test2'); // 使用对象 test1.getName(); // 打印: 名称: test1 test2.getName(); // 打印: 名称: test2
上述例子定义一个工厂函数,逻辑代码封装在testProduct类中,暴露出createProduct方法,调用时传入不同的参数返回不同的内容。
-
构造器模式
特点:定义一个通用的构造函数,方便多次传递参数调用。
优点:减少代码重复量,提高可维护性和可拓展性。
示例:class testPerson { constructor(name, age,) { this.name = name; this.age = age; } introduce() { console.log(`姓名: ${this.name}, 年龄: ${this.age}`); } } const test1 = new testPerson('张三', 18); test1.introduce(); // 姓名: 张三, 年龄: 18 const test2 = new testPerson('李四', 20); test2.introduce(); // 输出: 姓名: 李四, 年龄: 20
定义一个testPerson类,每次传入不同参数即可创建不同的用户对象,如果后续需要修改属性只需要调整testPerson类。
前端设计模式-结构型
结构型:主要针对对象之间的组合。通过增加代码的复杂度,从而提高扩展性和适配性,使代码兼容性更好、使某个方法功能更强大。
-
适配器模式
特点:使某个类的接口有更强的适配性,比如本来支持mp3格式的,现在适配成能支持mp4格式的。
优点:适配扩展后提高了复用性,降低耦合度并且增强了灵活性。
示例:// ------ 本来存在需要被适配的mp3接口 ------ class Receptacle { plugIn() { console.log("mp3"); } } // ------ 适配者类 ------ class ForeignReceptacle { plugInMp4() { console.log("mp4"); } } // ------ 用于适配的方法 ------ class VoltageAdapter { constructor(foreignReceptacle) { this.foreignReceptacle = foreignReceptacle; } plugIn() { this.foreignReceptacle.plugInMp4(); } }
使用适配器代码:正常使用Receptacle类输出mp3,如果要适配mp4,那么使用定义的VoltageAdapter适配器把mp4的ForeignReceptacle类适配到Receptacle上。 这个方法扩展了Receptacle类的功能,也不需要修改Receptacle类。
// 创建mp3 const receptacle = new Receptacle(); receptacle.plugIn(); // 打印输出: mp3 // 创建mp4 const foreignReceptacle = new ForeignReceptacle(); // 使用适配器将 mp4 适配到 mp3 const adapter = new VoltageAdapter(foreignReceptacle); adapter.plugIn(); // 打印输出: mp4
-
装饰器模式
特点:创建一个对象去包裹原始对象,在不修改原始对象本身的情况下,动态的给指定对象添加新功能。
优点:不改动原函数的情况下方便动态扩展功能,可以服用现有函数增强灵活性。
示例:// 基础函数 function getGreet(name) { console.log(`你好啊,${name}!`); } // 装饰器函数 function welcomePrefix(greetFunction) { return function(name) { console.log("欢迎"); greetFunction(name); }; } // 基础函数 getGreet("张三"); // 打印: 你好啊,张三! // 添加 欢迎啊 前缀 const setWelcome = welcomePrefix(getGreet); setWelcome("张三"); // 打印: 欢迎 // 打印: 你好啊,张三!
getGreet只能输出你好啊**,但是使用装饰器函数welcomePrefix装饰后,可以在前面添加“欢迎啊”的前缀,通过这个实现思路方式不需要修改基础函数就能添加功能。
-
代理模式
特点:给某个对象加一个代理对象,代理对象起到中介作用,中介对象在不改变原来对象情况下添加功能。
优点:代理对象可以很方便实现拦截控制访问,并且不能修改原对象提高代码复用率。
示例:// 基础函数 function counterEvent() { let count = 0; return { setCount: () => { count += 1; }, getCount: () => { return count; } }; } // 代理函数 function countProxy() { const newCounter = counterEvent(); return { setCount: () => { newCounter.setCount(); }, getCount: () => { return newCounter.getCount(); } }; } // 创建一个代理对象 const myCounter = countProxy(); // 触发增加 myCounter.setCount(); myCounter.setCount(); myCounter.setCount(); // 获取当前数 console.log(myCounter.getCount()); // 打印: 3
代理模式不让用户直接操作原始函数counterEvent,而是通过代理函数countProxy去操作函数counterEvent。
前端设计模式-行为型
行为型:主要针对对象之间的交互。针对特定的应用场景,通过封装指定对象之间的交互方式规则,使对象之间协作更加灵活高效健壮。
-
观察者模式
特点:观察某个对象是否发生变化,如果发生变化就会通知所有订阅者,并作出相应的操作,是一对一或一对多的关系。
优点:有很强动态灵活性,可以很容易添加或移除观察者,把观察者和被观察者解耦进行逻辑分离易于维护。
示例:// 观察者 class Sub { constructor() { this.observers = []; } add(observer) { // 添加观察者到列表中 this.observers.push(observer); } remove(observer) { // 从列表中移除观察者 this.observers = this.observers.filter(obs => obs !== observer); } notify(msg) { // 通知所有观察者 this.observers.forEach(observer => observer(msg)); } } // 用于创建观察者 const createObs = (name) => { return (msg) => { console.log(`${name} 收到: ${msg}`); }; };
被观察者Sub里面有add(添加)、remove(移除)和notify(通知)观察者的方法,观察者createObs里面有接收通知的方法。 使用sub.add 添加观察者后可以使用sub.notify发布消息通知所有观察者。 使用sub.remove移除观察者后不会再收到通知。
// 创建一个被观察者 const sub = new Sub(); // 创建观察者 const obs1 = createObs("观察者1"); const obs2 = createObs("观察者2"); // 订阅被观察者 sub.add(obs1); sub.add(obs2); // 发布消息 sub.notify("你好!"); // 观察者1和观察者2都收到: 你好! // 移除观察者1 sub.remove(obs1); // 再次发布 sub.notify("你好!"); // 只有观察者2收到: 你好!
-
发布订阅者模式
特点:这个模式与观察者模式类似,但观察者模式是一对一或一对多,发布订阅者模式是多对多的关系,应用场景有所不同。
优点:多对多关系有很强的动态灵活性,一个事件可以多个订阅者,一个订阅者可以订阅多个事件,把发布者和订阅者完全解耦提高灵活性和扩展性。
示例:// 发布者 class Pub { constructor() { this.subobj = {}; } subscribe(event, callback) { // 订阅事件 if (!this.subobj[event]) { this.subobj[event] = []; } this.subobj[event].push(callback); } unsubscribe(event, callback) { // 移除订阅事件 if (this.subobj[event]) { this.subobj[event] = this.subobj[event].filter(cb => cb !== callback); } } publish(event, data) { // 发布事件 if (this.subobj[event]) { this.subobj[event].forEach(callback => callback(data)); } } } // 创建一个发布者实例 const pub = new Pub(); // 订阅者回调函数 const subevent1 = (msg) => { console.log(`订阅者1 收到: ${msg}`); }; const subevent2 = (msg) => { console.log(`订阅者2 收到: ${msg}`); }; // 订阅事件 pub.subscribe("greet", subevent1); pub.subscribe("greet", subevent2); // 发布消息 pub.publish("greet", "你好!"); // 订阅者1和订阅者2 收到: 你好! // 移除一个订阅者 pub.unsubscribe("greet", subevent1); // 再次发布消息 pub.publish("greet", "你好!"); // 只有订阅者2 收到: 你好!
定义一个Pub类,里面有subscribe(添加订阅事件)、unsubscribe(移除订阅事件)、publish(发布订阅事件)。 new Pub() 创建一个发布者示例,可以添加、移除和发布事件。 this.subobj = {} 存放事件映射,而不是数组,{}里面的每个事件都可以存放一个订阅者数组从而实现多对多的关系。
-
命令模式
特点:把请求封装在对象里面整个传递给调用对象,使里面参数更加灵活方便易于扩展。
优点:使发送和接收者完全解耦独立易于数据维护、逻辑独立方便灵活处理、队列请求可以撤销操作。
示例:// 接收者 class testLight { on() { console.log("打开灯了"); } off() { console.log("关闭灯了"); } } // 命令基类 class Comm { constructor(receiver) { this.receiver = receiver; } } // 具体命令 class LightOnComm extends Comm { execute() { this.receiver.on(); } } class LightOffComm extends Comm { execute() { this.receiver.off(); } } // 调用者 class RemoteControl { onButton(comm) { comm.execute(); } }
接收者testLight主要负责执行业务逻辑命令,即决定是否关灯。 LightOnComm和LightOffComm继承Comm类,实现execute()方法,在其中分布调用on和off方法。 RemoteControl类负责调用者的方法,即去调用execute()方法。
// 使用 const testlight = new testLight(); const lightOnComm = new LightOnComm(testlight); const lightOffComm = new LightOffComm(testlight); const remoteControl = new RemoteControl(); remoteControl.onButton(lightOnComm); // 输出: 打开灯了 remoteControl.onButton(lightOffComm); // 输出: 关闭灯了
-
模板模式
特点:定义好整个操作过程的框架,框架中把每个步骤的逻辑独立处理。
优点:步骤独立分开管理,易于扩展功能维护代码。
示例:class Game { constructor(obj) { } initGame() { console.log('初始化'); } startGame() { console.log('游戏开始'); } onGame() { console.log('游戏中'); } endGame() { console.log('游戏结束'); } personEntry() { this.initGame() this.startGame() this.onGame() this.endGame() } }
这个Game类中把每个步骤的逻辑都放在对应步骤方法中,独立管理互不影响。添加或者减少步骤,只需要修改对应的方法即可。