本文深入解析JavaScript前端开发中常用的设计模式,涵盖创建型、结构型及行为型三大类。重点阐述SOLID五大基本原则,并详细解读单例、工厂、适配器、装饰器、策略、观察者及发布-订阅模式的核心概念、优缺点及代码实现,帮助开发者提升代码的可重用性、可读性与可靠性。
关于设计模式
软件设计模式,亦称设计模式,是一套经过反复使用、被广泛知晓且分类编目的代码设计经验总结。它旨在描述软件设计中反复出现的问题及其解决方案,即解决特定问题的一系列通用套路。这些模式源自前辈们的经验总结,具有普遍性和可复用性,主要目的是提升代码的可重用性、可读性以及可靠性。
简而言之,设计模式是一些经过长期实践考验得出的通用代码编写规范。遵循这些模式并借鉴前人经验,有助于编写出结构清晰、易于维护的高质量代码。
在设计模式的应用中,必须掌握五大基本原则(SOLID):
(1)单一职责原则(SRP):一个类应当仅有一个引起其变化的原因,即功能职责应单一明确。
(2)开放封闭原则(OCP):软件实体应对扩展开放,对修改关闭。
(3)里氏替换原则(LSP):基类出现的地方,子类一定可以出现。核心在于继承关系的正确性。
(4)接口隔离原则(ISP):一个接口应代表一种角色,不该做的事情不做,该做的事要做。核心在于降低耦合度与依赖关系。
(5)依赖倒置原则(DIP):针对接口编程,依赖于抽象而非具体实现。对象不应与具体实例硬编码耦合,而应依赖抽象概念。
具体而言,应用设计模式带来的好处包括以下几点:
① 良好的封装性,防止内部变量污染外部状态。
② 封装后的代码可作为独立模块供外部调用。外部使用者无需了解内部细节,只需按照约定规范进行调用即可。
③ 遵循开放封闭原则,外部无法修改内部代码从而保证内部逻辑的正确性;同时提供扩展接口,提高了系统的灵活性。
目前主流的前端框架如React、Vue等均应用了不同的设计模式。例如,Vue中广泛使用了观察者模式和发布-订阅模式。
七种常见的设计模式
设计模式总共分为3大类共23种,本文主要介绍前端开发中常用的几种模式。
① 创建型模式:包括单例模式、工厂模式、建造者模式;
② 结构型模式:包括适配器模式、装饰器模式、代理模式;
③ 行为型模式:包括策略模式、观察者模式、发布订阅模式、职责链模式、中介者模式。
单例模式
单例模式确保一个类只有一个实例,并提供一个全局访问点,即一个类只生成唯一的实例对象。
实现方式通常是在类中声明一个属性instance。当调用getInstance函数时,判断instance是否已存在实例:若存在则直接返回该instance对象;若不存在则创建新实例。
class Singleton {
let _instance = null;
static getInstance() {
if (!Singleton._instance) {
Singleton.instance = new Singleton()
}
// 如果这个唯一的实例已经存在,则直接返回
return Singleton._instance
}
}
const s1 = Singleton.getInstance()
const s2 = Singleton.getInstance()Vuex 是单例模式的典型应用,其 store 对象即为单例实例。
通过分析其功能代码,我们可以明确单例模式的优缺点。
优点:节约系统资源,保证访问的一致性。
缺点:扩展性较差,因为单例模式通常自行实例化,缺乏统一的接口支持。
工厂模式
该模式在前端开发中非常常用。通过声明一个类,根据传入的参数生成对应的实例对象,即为工厂模式。每个类如同一个已开设好的工厂,用户只需提出需求,工厂便会生成所需的对象并返回。
class Restaurant{
constructor(){
this.menuData = {};
}
// 获取菜品
getDish(dish){
if(!this.menuData[menu]){
console.log("菜品不存在,获取失败");
return;
}
return this.menuData[menu];
},
// 添加菜品
addMenu(menu,description){
if(this.menuData[menu]){
console.log("菜品已存在,请勿重复添加");
return;
}
this.menuData[menu] = menu;
}
// 移除菜品
removeMenu(menu){
if(!this.menuData[menu]){
console.log("菜品不存在,移除失败");
return;
}
delete this.menuData[menu];
},
}
class Dish{
constructor(name,description){
this.name = name;
this.description = description;
}
eat(){
console.log(`I'm eating ${this.name},it's ${`this.description);
}
}优点:
- 良好的封装性,访问者无需了解具体的创建过程,代码结构清晰。
- 扩展性良好,通过工厂方法隔离了用户与创建流程,符合开闭原则。
- 解耦了高层逻辑与底层产品类,符合最少知识原则,即不需要的交互不进行。
缺点:
如果类定义过于抽象或复杂,可能会导致代码可读性下降。
适配器模式
该模式易于理解,类似于日常使用的产品(如投影仪)。当现有电线无法直接适配屏幕时,需借助中间适配器使两者能够通信。
interface bookDataType1 {
book_id: number;
status: number;
create: string;
update: string;
}
interface bookDataType2 {
id: number;
status: number;
createTime: number;
updateAt: string;
}
interface bookDataType3 {
book_id: number;
status: number;
createTime: number;
updateAt: number;
}
const getTimeStamp = function (str: string): number {
//.....转化成时间戳
return timeStamp;
};
//适配器
export const bookDataAdapter = {
adapterType1(list: bookDataType1[]) {
const bookDataList: bookData[] = list.map((item) => {
return {
book_id: item.book_id,
status: item.status,
createAt: getTimeStamp(item.create),
updateAt: getTimeStamp(item.update),
};
});
return bookDataList;
},
adapterType2(list: bookDataType2[]) {
const bookDataList: bookData[] = list.map((item) => {
return {
book_id: item.id,
status: item.status,
createAt: item.createTime,
updateAt: getTimeStamp(item.updateAt),
};
});
return bookDataList;
},
adapterType3(list: bookDataType3[]) {
const bookDataList: bookData[] = list.map((item) => {
return {
book_id: item.book_id,
status: item.status,
createAt: item.createTime,
updateAt: item.updateAt,
};
});
return bookDataList;
},
};优势在于提升原有逻辑复用率,有助于避免大规模改写现有代码,这是一种在不改动原有代码前提下做出的妥协;
缺点:会让系统变得零乱,明明调用 A,却被适配到了 B,如果滥用,那么对可阅读性不太友好。简而言之搞复杂了,所以通常建议不要出现以上这样的格式问题,应该跟后端沟通好数据。
装饰器模式
典型的大肠包小肠,当前使用的对象无法满足我们的全部需求,于是乎我们建一个新的类,再把这个对象在类中进行扩展,再生成一个新的对象。

策略模式
这个是相当相当常用,而且很好用的一个设计模式,可以让我们根据不同的选择去实现对应的功能,省略了大量的if,else。
比如我们现在有一个需求判断,比如我现在要根据别人给我的不同食材去制造料理,最暴力常规那就是if,else多写几个就解决了。但是这里如果我们用策略模式就可以用很清晰,很简洁的代码去解决这个问题。
if ( 'food' == '苹果') {
水煮()
} else if ('food' == '胡萝卜') {
炒了()
} else if ('food' == '鱼') {
清蒸()
} else if ('food' == '猪肉') {
炸了()
} else if ('food' == '牛肉') {
烤了()
} else {
生吃()
}
// 用了策略模式,看起来舒服多了
let wayObj = {
'苹果': 水煮(),
'胡萝卜': 炒了(),
'鱼': 清蒸(),
'猪肉': 炸了(),
'牛肉': 烤了(),
'不知道': 生吃()
}观察者模式
这个模式从名字就可以看出来它是干嘛的,观察者重点就是观察,有两个对象,一个是观察,一个是被观察,被观察发生了变化,那我们观察的对象就可以知道这个变化。
观察者模式有一个别名叫“发布-订阅模式”,或者说是“订阅-发布模式”,订阅者和订阅目标是联系在一起的,当订阅目标发生改变时,逐个通知订阅者。我们可以用报纸期刊的订阅来形象的说明,当你订阅了一份报纸,每天都会有一份最新的报纸送到你手上,有多少人订阅报纸,报社就会发多少份报纸,报社和订报纸的客户就是上面文章开头所说的“一对多”的依赖关系。
// 观察者模式 被观察者Subject 观察者Observer Subject变化 notify观察者
let observerIds = 0;
// 被观察者Subject
class Subject {
constructor() {
this.observers = [];
}
// 添加观察者
addObserver(observer) {
this.observers.push(observer);
}
// 移除观察者
removeObserver(observer) {
this.observers = this.observers.filter((obs) => {
return obs.id !== observer.id;
});
}
// 通知notify观察者
notify() {
this.observers.forEach((observer) => observer.update(this));
}
}
// 观察者Observer
class Observer {
constructor() {
this.id = observerIds++;
}
update(subject) {
// 更新
}
}发布-订阅模式
其实上面也说了,跟观察者模式是有异曲同工之妙的,但是它可以是一个一对多的关系,而且它需要一个中间人。
class Event {
constructor() {
this.eventEmitter = {};
}
// 订阅
on(type, fn) {
if (!this.eventEmitter[type]) {
this.eventEmitter[type] = [];
}
this.eventEmitter[type].push(fn);
}
// 取消订阅
off(type, fn) {
if (!this.eventEmitter[type]) {
return;
}
this.eventEmitter[type] = this.eventEmitter[type].filter((event) => {
return event !== fn;
});
}
// 发布
emit(type) {
if (!this.eventEmitter[type]) {
return;
}
this.eventEmitter[type].forEach((event) => {
event();
});
}
}到此这篇关于深入浅出JavaScript前端中的设计模式的文章就介绍到这了,更多相关JS设计模式内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!