一、前端设计模式概述
前端设计模式是对软件设计中普遍存在问题的解决方案,是一套可重用的代码设计经验总结,有助于提升代码质量和可维护性。
在前端开发中,设计模式可以分为创建型、结构型和行为型三种类型。创建型设计模式用于创建对象,如工厂模式、单例模式等。工厂模式可以隐藏创建对象的复杂性,只对外提供一个接口,实现构造函数和创建者的分离,满足开闭原则。例如,jQuery 的选择器底层实现机制就是工厂模式,通过$函数可以方便地创建 jQuery 对象,同时去除了new操作的复杂弊端,还能实现链式操作。单例模式保证一个类只有一个实例,在前端开发中常用于缓存、全局状态管理等场景。比如在实现请求缓存时,可以使用单例模式创建一个Request类,确保全局只有一个请求缓存对象。
结构型设计模式用于解决类和对象的组合问题,如代理模式、装饰器模式等。代理模式可以自定义控制对元对象的访问方式,并且允许在更新前后做一些额外处理。在前端框架实现中,代理模式可以用于监控、代理工具等场景。装饰器模式可以在不改变原对象的基础上,为对象添加额外的功能。例如,可以使用装饰器模式为一个函数添加日志记录功能。
行为型设计模式用于描述对象之间的通信,如观察者模式、策略模式等。观察者模式是一种订阅机制,可在被订阅对象发生改变时通知订阅者。在前端开发中,观察者模式常用于实现事件订阅和发布,如邮件订阅、上线订阅等场景。策略模式通过将条件判断转换为一个个策略对象,减少了if...else的数量,提升了代码的可读性和可维护性。例如,在实现计算器功能时,可以使用策略模式根据用户的操作类型选择不同的计算策略。
二、常见设计模式详解
(一)单例模式
- 定义及需求:保证一个类只有一个实例,提供全局访问点,适用于线程池、全局缓存等场景。
单例模式在前端开发中有广泛的应用场景。例如,在全局状态管理中,如 Vuex 的全局 Store 和 Redux 的 Store,它们都是单例模式的应用。这些全局状态管理工具采用单一状态树,一个对象即为唯一数据源,确保整个应用中只有一个实例存在,方便在不同组件之间共享和管理状态。
- 实现方式:用变量标识是否已创建对象,若已创建则直接返回。
实现单例模式的一种常见方式是在构造函数内部进行实例化控制。例如:
function SingleDemo() {
if (!singleDemo) {
singleDemo = this;
}
return singleDemo;
}
SingleDemo.prototype.show = function () {
console.log("我是单例模式");
};
const single1 = new SingleDemo();
const single2 = new SingleDemo();
console.log(single1 === single2);
另一种方式是使用类的静态属性:
class Singleton {
static instance;
constructor() {
if (!Singleton.instance) {
Singleton.instance = this;
}
return Singleton.instance;
}
}
const instance1 = new Singleton();
const instance2 = new Singleton();
console.log(instance1 === instance2);
- 优点:划分命名空间,减少全局变量,实例化一次多次调用均为同一实例。
单例模式有诸多优点。首先,它可以划分命名空间,避免全局变量的滥用,使代码更加清晰和可维护。其次,由于只实例化一次,多次调用都是同一个实例,因此可以节约系统资源,特别是在需要频繁创建和销毁对象的场景下,能显著提高系统性能。例如,在全局缓存的应用中,单例模式可以确保只有一个缓存对象,避免了重复创建缓存带来的资源浪费。
- 案例分析:Vue 中的 Element UI 全屏 Loading 和 Vuex 的全局 Store 应用。
在 Element UI 中,全屏 Loading 的实现就用到了单例模式。当用户传的options的fullscreen为true且已经创建了单例,则直接返回之前创建的单例,如果之前没有创建过,则创建单例并赋值给闭包中的fullscreenLoading后返回新创建的单例实例。在 Vuex 中,store就是一个单例模式,采用了单一状态树,确保整个应用中只有一个数据源,方便管理和共享应用的状态。
(二)工厂模式
- 简单工厂:根据参数创建同一类对象,优点是获取对象方便,缺点是对象多时难以维护。
简单工厂模式是工厂模式的一种简单形式。例如:
let factory = function (type) {
function basketball() {
this.text = '篮球';
}
function football() {
this.text = '足球';
}
switch (type) {
case 'basketball':
return new basketball();
break;
case 'football':
return new football();
break;
default:
throw new Error('错误');
}
};
let basketball = factory('basketball');
let football = factory('football');
简单工厂根据参数的不同生成不同的对象,获取对象非常方便。但是当需要创建的对象种类很多时,简单工厂的代码会变得难以维护。
- 工厂方法:将创建对象工作推迟到子类,只负责实例化对象,方便扩展。
工厂方法模式将创建对象的工作推迟到子类中。例如:
let factory = function (type) {
if (this instanceof factory) {
var s = new this[type]();
return s;
} else {
return new factory(type);
}
};
factory.prototype = {
basketball: function () {
this.text = '篮球';
},
football: function () {
this.text = '足球';
}
};
let basketball = factory('basketball');
let football = factory('football');
在工厂方法模式中,工厂类只负责实例化对象,具体的创建工作由子类完成。这样当需要添加新的产品类型时,只需要创建一个新的子类,而不需要修改工厂类的代码,方便了扩展。
- 案例分析:Vue 中的 VNode 和 vue-route 的路由创建。
在 Vue 中,虚拟 DOM(VNode)的创建可以看作是工厂模式的应用。VNode 是对真实 DOM 的一种抽象,通过工厂方法可以根据不同的元素类型创建不同的 VNode 对象。在 vue-route 中,路由的创建也可以使用工厂模式,根据不同的路由配置生成相应的路由对象。
(三)策略模式
- 定义及特点:封装一系列算法,根据输入调整采用的算法,实现和使用分离。
策略模式定义了一系列算法,把它们一个个封装起来,并且可以相互替换。例如:
let strategies = {
"S": function (salary) {
return salary * 4;
},
"A": function (salary) {
return salary * 3;
},
"B": function (salary) {
return salary * 2;
},
"C": function (salary) {
return salary * 1;
}
};
// 引用
let calculateBonus = function (level, salary) {
return strategies[level](salary);
};
calculateBonus('S', 20000);
在这个例子中,根据不同的输入参数level,选择不同的策略函数来计算年终奖金,实现了算法的使用和实现的分离。
- 案例分析:Element UI 表格 formatter 和表单验证场景。
在 Element UI 的表格中,可以使用策略模式来实现不同的格式化函数。根据表格数据的不同类型,选择不同的格式化策略,将数据格式化为合适的显示形式。在表单验证中,也可以使用策略模式来定义不同的验证规则。例如,对于不同类型的输入字段,可以定义不同的验证策略,如必填验证、邮箱格式验证等。
(四)代理模式
- 定义及作用:为目标对象创造代理对象,控制对目标对象的访问,进行预操作和后操作。
代理模式是一种结构型设计模式,它允许在不改变原始对象的情况下,通过引入一个代理对象来控制对原始对象的访问。代理对象充当原始对象的中介,客户端与代理对象交互,代理对象再将请求转发给原始对象,并可以在请求前后进行一些额外的处理。
- 案例分析:拦截器在 HTTP 和路由跳转中的应用。
在前端开发中,拦截器是代理模式的一种常见应用。例如,在 HTTP 请求中,可以使用拦截器来在请求发送前添加请求头、进行参数校验等预操作,在响应返回后进行错误处理、数据转换等后操作。在路由跳转中,也可以使用代理模式来实现权限控制。例如,当用户没有权限访问某个页面时,代理对象可以拦截路由跳转,提示用户没有权限访问。
三、设计模式优缺点对比
(一)单例模式优缺点
- 优点:减少全局变量,方便管理命名空间。
单例模式通过将全局变量的创建和管理集中在一个地方,避免了全局变量的滥用,使得代码的命名空间更加清晰,减少了命名冲突的可能性。同时,由于只有一个实例存在,对于一些需要共享资源的场景,如全局配置、缓存等,可以方便地进行管理和访问。
- 缺点:可能导致代码耦合度高,不利于测试。
单例模式的实现通常会将实例的创建和管理封装在一个类内部,这可能导致其他部分的代码对单例类产生强依赖,增加了代码的耦合度。在进行单元测试时,由于单例的唯一性,很难对依赖单例的代码进行独立测试,需要使用一些特殊的测试技巧,如模拟单例对象等。
(二)工厂模式优缺点
- 简单工厂优点:获取对象方便,无需知道创建细节。
简单工厂模式通过一个工厂函数,根据不同的参数返回不同类型的对象,使得客户端代码无需关心对象的具体创建过程,只需要调用工厂函数即可获取所需的对象。这种方式简化了客户端代码,提高了代码的可读性和可维护性。
- 简单工厂缺点:代码维护性差,不适合复杂场景。
当需要创建的对象种类较多时,简单工厂的工厂函数会变得庞大而复杂,包含大量的条件判断语句。这使得代码的维护性变差,难以扩展和修改。此外,简单工厂模式不符合开闭原则,当需要添加新的对象类型时,需要修改工厂函数的代码。
- 工厂方法优点:方便扩展,符合开闭原则。
工厂方法模式将对象的创建工作委托给子类,工厂类只负责定义创建对象的接口,具体的创建工作由子类实现。这种方式使得当需要添加新的对象类型时,只需要创建一个新的工厂子类,无需修改原有代码,符合开闭原则。同时,工厂方法模式也使得代码的结构更加清晰,易于扩展和维护。
- 工厂方法缺点:实现相对复杂,需要理解原型链等概念。
工厂方法模式的实现相对复杂,需要理解原型链、继承等面向对象编程的概念。此外,由于每个对象类型都需要一个对应的工厂子类,当对象类型较多时,会导致代码量增加,类的层次结构也会变得复杂。
(三)策略模式优缺点
- 优点:代码复用性高,易于扩展和维护。
策略模式将不同的算法封装在独立的策略类中,使得这些算法可以相互替换,提高了代码的复用性。当需要添加新的算法时,只需要创建一个新的策略类,无需修改原有代码,易于扩展和维护。同时,策略模式也使得代码的结构更加清晰,易于理解和修改。
- 缺点:可能增加代码复杂度,需要一定的学习成本。
策略模式需要创建多个策略类,当策略类较多时,会增加代码的复杂度。此外,策略模式的实现需要一定的面向对象编程基础,对于初学者来说可能有一定的学习成本。
(四)代理模式优缺点
- 优点:控制对象访问,方便进行预处理和后处理。
代理模式通过引入一个代理对象,控制对目标对象的访问。代理对象可以在请求发送前进行预处理,如参数校验、日志记录等,也可以在响应返回后进行后处理,如错误处理、数据转换等。这种方式使得代码的结构更加清晰,易于扩展和维护。
- 缺点:可能影响性能,增加系统复杂性。
代理模式需要创建一个代理对象,当代理对象的功能较为复杂时,可能会影响系统的性能。此外,代理模式也增加了系统的复杂性,需要理解代理对象和目标对象之间的关系,以及代理对象的实现原理。
四、前端框架中的设计模式应用总结
前端设计模式在不同的前端框架中有着广泛的应用,每种设计模式都为框架的开发和使用带来了独特的优势,但同时也存在一些不足之处。
(一)Vue.js 中的设计模式应用
- 单例模式:在 Vue.js 中,全局状态管理工具 Vuex 就是单例模式的应用。Vuex 采用单一状态树,整个应用只有一个 Store 实例,方便在不同组件之间共享和管理状态。这种设计使得状态的管理更加集中和高效,避免了状态的混乱和不一致。
- 工厂模式:Vue.js 中的虚拟 DOM(VNode)的创建可以看作是工厂模式的应用。根据不同的元素类型,通过工厂方法创建不同的 VNode 对象,实现了对真实 DOM 的抽象和高效操作。
- 策略模式:在 Vue.js 的表单验证中,可以使用策略模式来定义不同的验证规则。例如,对于不同类型的输入字段,可以定义不同的验证策略,如必填验证、邮箱格式验证等,提高了代码的复用性和可维护性。
- 代理模式:在 Vue.js 中,对 DOM 操作的代理是代理模式的应用。通过代理对象来控制对 DOM 的访问,实现了对 DOM 操作的优化和管理。
(二)React 中的设计模式应用
- 工厂模式:React 中的组件创建可以看作是工厂模式的应用。通过工厂函数或类,根据不同的参数创建不同类型的组件,提高了组件的复用性和可维护性。
- 策略模式:在 React 的状态管理中,可以使用策略模式来处理不同的状态更新策略。例如,根据不同的业务需求,选择不同的状态更新方式,提高了代码的灵活性和可维护性。
- 代理模式:在 React 的性能优化中,可以使用代理模式来实现对组件的懒加载和预加载。通过代理对象来控制组件的加载时机,提高了应用的性能和用户体验。
(三)Angular 中的设计模式应用
- 依赖注入:Angular 中的依赖注入是一种设计模式,它允许将对象的创建和管理交给框架,而不是在代码中显式地创建对象。这种方式提高了代码的可测试性和可维护性。
- 模块模式:Angular 中的模块是一种设计模式,它将应用程序划分为不同的模块,每个模块负责特定的功能。这种方式提高了代码的可维护性和可扩展性。
- 策略模式:在 Angular 的表单验证中,可以使用策略模式来定义不同的验证规则。与 Vue.js 和 React 类似,提高了代码的复用性和可维护性。
(四)设计模式的选择与注意事项
- 根据实际需求选择:开发者应根据项目的实际需求选择合适的设计模式。例如,对于需要全局状态管理的项目,可以选择单例模式;对于需要灵活处理不同算法的场景,可以选择策略模式。
- 注意性能影响:虽然设计模式可以提高代码的质量和可维护性,但在某些情况下,可能会对性能产生一定的影响。例如,代理模式可能会增加系统的复杂性和性能开销,因此在使用时需要权衡利弊。
- 避免过度设计:在使用设计模式时,应避免过度设计。设计模式是为了解决特定的问题而存在的,如果过度使用设计模式,可能会导致代码变得复杂和难以理解。
- 不断学习和实践:设计模式是一种不断发展和演变的技术,开发者应不断学习和实践,掌握不同设计模式的应用场景和实现方法,提高自己的编程水平。
总之,前端设计模式在前端框架中有着广泛的应用,开发者应根据实际需求选择合适的设计模式,以提高代码质量和可维护性。同时,也应注意性能影响和避免过度设计,不断学习和实践,掌握更多的设计模式和编程技巧。