前端设计模式
一.设计模式概览
设计模式是针对软件设计开发过程中反复出现的某类问题的通用解决方案。设计模式更多的是指导思想和方法论,而不是现成的代码,每种设计模式都有每种语言中的具体实现方式。学习设计模式更多是理解各个模式的内在思想和解决的问题,代码实现则是对加深理解的辅助。
设计模式分为三大类:
1.创建型模式:处理对象的创建,根据实际情况使用合适的方式创建对象。常规的处对象创建方式可能会导致设计上的问题,或增加设计的复杂度。创建型模式通过以某种方式控制对象的创建来解决问题。
2.结构型模式:通过识别系统中组件间简单关系来简化系统的设计。
3.行为型模式:用于识别对象之间的常见的交互模式并加以实现,如此,增加了这些交互的灵活性。
设计模式一共有23种,但作为前端开发人员,需要了解以下10种。
二.设计模式分类
1.创建型模式
1.1 工厂模式
概念:通过函数封装对象的创建过程,而不是直接使用构造函数或类。
核心思想:是将对象的创建逻辑集中在一个函数中,从而提高代码的可复用性、灵活性和维护性。
特点:
-
封装创建逻辑:将对象的创建过程封装在一个函数中,隐藏实现的细节。
-
避免new关键字:工厂函数不需要使用new关键字来创建对象。
-
返回新对象:工厂函数通常会返回一个新创建的对象。
-
灵活性:可以根据不同的参数返回不同类型的对象。
优势:
-
简化对象创建:将复杂的创建逻辑集中在一个地方。
-
提高代码的复用性:可以在多个地方调用工厂函数来创建对象。
-
增强灵活性:可以根据不同的条件返回不同的对象。
-
避免构造函数的问题:工厂函数不需要使用new,避免了构造函数中的this绑定问题。
使用场景:
-
需要创建多个相似的对象:工厂函数可以简化对象的创建过程。
-
需要封装复杂创建逻辑:将对象的创建逻辑集中在一个地方。
-
需要动态返回不同类型的对象:根据参数返回不同的对象。
-
需要实现私有状态:利用闭包实现对象的私有状态。
总结:
工厂函数是一种灵活且强大的设计模式,适用于需要封装对象和创建逻辑的场景。避免了构造函数的一些问题如(this绑定),同时也提供了更高的灵活性和可复用性,如果需要创建复杂的对象或实现私有状态,推荐使用工厂函数。
代码实现:
1.基本工厂函数
以下是一个简单的工厂函数示例,用于创建用户对象:
function createUser(name, age) {
return {
name,
age,
greet() {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
},
};
}
// 使用工厂函数创建对象
const user1 = createUser("Alice", 25);
const user2 = createUser("Bob", 30);
user1.greet(); // 输出: Hello, my name is Alice and I am 25 years old.
user2.greet(); // 输出: Hello, my name is Bob and I am 30 years old.
2.带私有状态的工厂函数
工厂函数可以创建带有私有状态的对象,利用闭包实现:
function createCounter() {
let count = 0; // 私有状态
return {
increment() {
count++;
console.log(`Count: ${count}`);
},
decrement() {
count--;
console.log(`Count: ${count}`);
},
};
}
// 使用工厂函数创建计数器
const counter = createCounter();
counter.increment(); // 输出: Count: 1
counter.increment(); // 输出: Count: 2
counter.decrement(); // 输出: Count: 1
3.动态的返回不同类型的对象
工厂函数可以根据参数动态返回不同类型的对象:
function createShape(type, size) {
switch (type) {
case "circle":
return {
type,
radius: size,
area() {
return Math.PI * this.radius ** 2;
},
};
case "square":
return {
type,
sideLength: size,
area() {
return this.sideLength ** 2;
},
};
default:
throw new Error("Unknown shape type");
}
}
// 使用工厂函数创建不同形状
const circle = createShape("circle", 5);
const square = createShape("square", 4);
console.log(circle.area()); // 输出: 78.53981633974483
console.log(square.area()); // 输出: 16
4.组合工厂函数
工厂函数可以组合使用,创建更复杂的对象:
function createAddress(city, country) {
return {
city,
country,
};
}
function createUser(name, age, city, country) {
const address = createAddress(city, country); // 使用另一个工厂函数
return {
name,
age,
address,
greet() {
console.log(`Hello, I am ${this.name} from ${this.address.city}, ${this.address.country}.`);
},
};
}
// 使用工厂函数创建用户
const user = createUser("Alice", 25, "New York", "USA");
user.greet(); // 输出: Hello, I am Alice from New York, USA.
1.2单例模式
概念:确保一个类只有一个实例,并提供一个全局访问点来访问该实例。单例模式通常用于管理全局状态、共享资源或避免重复创建对象。
核心思想:
-
唯一实例:确保一个类只有一个实例。
-
全局访问:提供一个全局访问点,方便其他代码访问该实例。
代码实现:
1.使用对象字面量
对象字面量本身就是单例的,因为它在全局范围内只存在一个实例。
const Singleton = {
property:"value",
method(){
console.log('This is a singleton method.')
}
}
Singleton.method(); // 调用单例方法
这种方式简单直接,但是缺乏封装性。
2.使用闭包
通过闭包和立即执行函数(IIFE)实现单例模式。
const Singleton = (function () {
let instance;
function createInstance() {
const object = new Object("I am the instance");
return object;
}
return {
getInstance() {
if (!instance) {
instance = createInstance();
}
return instance;
}
};
})();
const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();
console.log(instance1 === instance2); // true,两个变量指向同一个实例
这种方式通过闭包隐藏了实例的创建逻辑,确保只有一个实例被创建。
3.使用ES6类
在ES6中,可以通过类的静态属性和方法实现单例模式。
class Singleton {
constructor() {
if (Singleton.instance) {
return Singleton.instance;
}
Singleton.instance = this;
this.property = "value";
}
method() {
console.log("This is a singleton method.");
}
}
const instance1 = new Singleton();
const instance2 = new Singleton();
console.log(instance1 === instance2); // true,两个变量指向同一个实例
这种方式利用了类的静态属性来存储单例实例。
4.使用模块化
在前端开发中,模块化如(ES6模块)支持单例模式,因为模块在第一次导入时就被缓存,后续导入会直接使用缓存的结果。
// singleton.js
let instance;
module.exports = class Singleton {
constructor() {
if(instance){
return instance;
}
instance = this;
this.property = 'value'
}
name() {
console.log('This is a singleton method');
}
}
// main.js
let Singleton = require("./singleton");
const instance1 = new Singleton();
const instance2 = new Singleton();
console.log(instance1 === instance2); // true
这种方式非常适合模块化开发。
使用场景:
-
全局状态管理:在前端框架中,单例模式常用于全局状态(Redux的store或Vue中的store)。
-
共享资源:例如,管理WebSocket连接,数据库库连接池等,确保资源不会重复创建。
-
工具类:例如,日志工具、配置管理器等,通常只需要一个实例。
-
避免重复渲染:在某些场景下,避免重复创建相同的DOM元素或组件实例。
优点:
-
节省资源:避免重复创建对象,节省内存和计算资源。
-
全局访问:方便在应用的任何地方访问单例实例。
-
一致性:确保全局状态或资源的一致性。
缺点:
-
难以测试:单例模式隐藏了依赖关系,可能导致单元测试困难。
-
违反单一职责原则:单例类通常及负责业务逻辑,又负责实例管理。
-
全局状态污染:过度使用单例模式可能导致全局状态混乱,增加代码耦合度。
总结:
单例模式适用于需要全局唯一实例的场景。在前端开发中,它常用于管理全局状态、共享资源或工具类。然而,单例模式也可能带来测试困难和全局状态污染的问题,因此需要谨慎使用。实际开发中,可以结合模块化、闭包或类来实现单例模式,以满足具体需求。
1.3原型模式
概念:用于通过复制现有的对象来创建新对象,而不是通过实例化类。这种模式适合用于需要频繁创建相似对象的场景,能够提升性能并减少资源消耗。
核心思想:
-
原型对象:作为模板的对象,新对象通过复制它来创建。
-
克隆方法:用于复制原型对象的方法,通常实现为clone()。
代码实现:
// 原型对象
const carPrototype = {
wheels: 4,
start() {
console.log('Car started');
},
stop() {
console.log('Car stopped');
}
};
// 克隆方法
function createCar() {
return Object.create(carPrototype);
}
// 创建新对象
const car1 = createCar();
const car2 = createCar();
console.log(car1.wheels); // 输出: 4
car1.start(); // 输出: Car started
优点:
-
性能提升:通过复制对象避免重复的初始化操作。
-
简化对象创建:无需依赖构造函数或类。
-
动态性:可以在运行时动态的添加或修改对象的属性和方法。
缺点:
-
深拷贝问题:默认的Object.create()是浅拷贝,复杂对象可能需要手动实现深拷贝。
-
复杂性增加:管理原型对象和克隆方法可能会增加代码复杂度。
使用场景:
-
频繁创建相似对象:如游戏中的敌人、子弹等。
-
避免重复初始化:当对象初始化成本比较高的时候。
-
动态配置对象:需要在运行时调整对象属性时。
总结:
原型模式通过复制现有对象来创建新对象,适用于频繁创建相似对象的场景,能够提升性能简化对象创建过程。
2.结构型模式
2.1 装饰器模式
概念:它允许不再修改原始对象的基础上,动态地扩展对象的功能。通过将对象包装在装饰器类中,装饰器模式为对象添加新的行为或修改现有行为,同时确保接口的一致性。
核心思想:
-
不修改原对象:通过“包装”的方式增强功能,避免继承带来的类的爆炸问题。
-
动态扩展:可以在运行时灵活地组合功能。
-
单一职责原则:每个装饰器专注于一个特定的功能。
使用场景:
-
动态添加功能:如日志记录、权限校验、缓存、性能监控等。
-
避免子类膨胀:当需要多种功能组合时,继承会导致类数量指数级增长。
-
临时增强对象:例如为某个按钮添加临时的加载状态或禁用状态。
实现方式:
在前端中,装饰器模式可以通过两种方式实现:
-
基于高阶函数/高阶组件(HOC,常见于react)。
-
基于ES7装饰器语法(通过@decorator)。
代码实现:
1.简单代码实现
class Circle {
draw() {
console.log('画一个圆形');
}
}
class Decorator {
constructor(circle) {
this.circle = circle;
}
draw() {
this.circle.draw();
this.setRedBorder(circle);
}
setRedBorder(circle) {
console.log('设置红色边框')
}
}
// 测试
let circle = new Circle();
let client = new Decorator(circle);
client.draw();
2.高阶函数(HOC)实现
// 基础组件
function Button() {
return <button>Click Me</button>;
}
// 装饰器:添加边框
function withBorder(Component) {
return function(props) {
return (
<div style={{ border: "2px solid red" }}>
<Component {...props} />
</div>
);
};
}
// 装饰器:添加点击日志
function withLogger(Component) {
return function(props) {
const handleClick = () => {
console.log("Button clicked!");
props.onClick?.();
};
return <Component {...props} onClick={handleClick} />;
};
}
// 组合装饰器
const DecoratedButton = withLogger(withBorder(Button));
// 使用
<DecoratedButton onClick={() => alert("Clicked!")} />
3.ES7装饰器语法
需要Babel插件(如@babel/plugin-proposal-decorators
)支持。
// 类装饰器:自动绑定方法
function autoBind(target) {
const proto = target.prototype;
const propertyNames = Object.getOwnPropertyNames(proto);
propertyNames.forEach(name => {
const descriptor = Object.getOwnPropertyDescriptor(proto, name);
if (typeof descriptor.value === 'function') {
proto[name] = descriptor.value.bind(target);
}
});
}
// 方法装饰器:记录执行时间
function logTime(target, name, descriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args) {
const start = performance.now();
const result = originalMethod.apply(this, args);
const end = performance.now();
console.log(`${name} executed in ${end - start}ms`);
return result;
};
return descriptor;
}
// 使用装饰器
@autoBind
class Calculator {
@logTime
add(a, b) {
return a + b;
}
}
const calc = new Calculator();
calc.add(2, 3); // 输出: add executed in 0.05ms
优点:
-
灵活扩展:无需修改原对象即可添加新功能。
-
组合自由:多个装饰器可以按需组合(如A(B(C(target)))。
-
符合开放封闭原则:对扩展开放,对修改关闭。
缺点:
-
复杂度增加:多层装饰器可能导致代码可读性下降。
-
调试困难:装饰器链的层级关系需要仔细跟踪。
-
ES7装饰器语法依赖工具链:需要Babel等工具支持。
使用场景:
-
React高阶组件(HOC):例如withRouter、connect(React-Redux)。
-
日志/性能监控:统一为方法添加日志记录或耗时统计。
-
表单校验:为输入组件动态添加校验逻辑。
-
权限控制:根据用户角色动态隐藏/禁用组件。
总结:
装饰器模式通过“包装”而非继承的方式,提供了一种灵活且可以维护的功能扩展方案。前端开发中,无论是通过高阶函数还是ES7装饰器语法,都能有效解决功能复用和动态增强的问题,是构建复杂应用时的常用设计模式。
2.2 适配器模式
概念:适配器模式是一种结构型设计模式,用于解决两个不兼容接口之间的兼容性问题。它通过将一个类的接口转换成客户端期望的另一个接口,使得原本因为接口不匹配而无法一起工作的类能够协同工作。
核心思想:
适配器模式的核心是转换。它通过引入一个适配器类,将目标接口与现有接口进行适配,使得客户端可以统一调用目标接口,而不需要关心底层实现。
使用场景:
-
集成第三方库:当需要使用第三方库,但其接口与现有系统不兼容时。
-
复用旧代码:在重构或升级系统时候,希望复用旧代码,但其接口与新系统不匹配。
-
统一接口:当需要统一多个类的接口时,适配器模式可以将它们转为一致的接口。
结构:
-
目标接口(Target):客户端期望的接口。
-
适配者(Adaptee):需要被适配的现有接口。
-
适配器(Adapter):实现目标接口,并持有适配者的引用,负责将目标接口调用转换为适配者的调用。
实现方式:
-
类适配器:通过继承适配者类来实现适配器。
-
对象适配器:通过组合适配者对象来实现适配器。
代码实现:
1.类适配器示例:
// 类适配器示例
// 目标接口
class Target {
request(){
throw new Error('Method not implemented');
}
}
// 适配者
class Adaptee {
specificRequest(){
return 'Adpatee specific request'
}
}
// 适配器(类适配器)
class Adapter extends Adaptee {
request(){
return this.specificRequest()
}
}
// 客户端
const adapter = new Adapter();
console.log(adapter.request());
2.对象适配器示例
// 目标接口
class Target {
request() {
throw new Error("This method must be overridden!");
}
}
// 适配者
class Adaptee {
specificRequest() {
return "Adaptee's specific request";
}
}
// 适配器(对象适配器)
class Adapter {
constructor(adaptee) {
this.adaptee = adaptee;
}
request() {
return this.adaptee.specificRequest();
}
}
// 客户端
const adaptee = new Adaptee();
const adapter = new Adapter(adaptee);
console.log(adapter.request()); // 输出: Adaptee's specific request
优点:
-
解耦:适配器模式将客户端与适配者解耦,客户端无需关心适配者的具体实现。
-
复用性:可以服用现有的类,而无需修改其代码。
-
灵活性:可以动态切换适配器,适应不同的需求。
缺点:
-
复杂性:引入适配器会增加系统的复杂性,尤其是在适配多个类时。
-
性能开销:适配器模式可能引入额外的调用层次,导致一定的性能开销。
总结: 适配器模式是一种非常有用的设计模式,特别适用于需要集成不兼容接口的场景,通过适配器模式,可以有效的复用现有代码,并保持系统的灵活性和可扩展性。
2.3代理模式
概念:
代理模式是一种结构型设计模式,它通过引入一个代理对象来控制另一个对象的访问。代理对象充当客户端和目标对象之间的中介,可以在不改变目标对象的情况下,增加额外的功能或控制访问。
核心思想:
代理模式的核心是间接访问。它通过代理对象简介访问目标对象,从而可以在访问过程中添加额外的逻辑。
-
延迟初始化(Lazy initialization)
-
访问控制(Access Control)
-
日志记录(Logging)
-
缓存(Caching)
-
远程代理(Remote Proxy)
使用场景:
-
延迟加载:当目标对象的创建或初始化成本较高时,可以用代理模式延迟加载。
-
访问控制:当需要对目标对象访问进行权限控制时,代理模式可以充当守卫。
-
日志记录:在访问目标对象时,代理可以记录日志或监控行为。
-
缓存:代理可以缓存目标对象的结果,避免重复计算或请求。
结构:
-
目标接口(Subject):定义目标对象和代理对象的共同接口,客户端通过该接口与目标对象交互。
-
目标对象(Real Subject):实际执行业务逻辑的对象。
-
代理对象(Proxy):实现目标接口,并持有目标对象的引用。它在调用目标对象之前或之后可以执行额外的操作。
代码实现:
1.简单示例:
/**
* pre:代理模式
* 小明追求A,B是A的好朋友,小明通过把花交给B,
* B拥有监听A的心情,当A的心情好的时候,再由B交给A.
*/
// 定义花的类
class Flower{
constructor(name){
this.name = name
}
}
// 小明拥有sendFlower的方法
let Xioaming = {
sendFlower(target){
var flower = new Flower("玫瑰花")
target.receive(flower)
}
}
let B = {
receive(flower){
this.flower =flower
A.listenMood(()=>{
A.receive(this.flower)
})
}
}
// A接收到花之后输出花的名字
let A = {
receive(flower){
console.log(`A收到了${flower.name} `)
// A收到了玫瑰花
},
listenMood(func){
setTimeout(func,1000)
}
}
Xioaming.sendFlower(B)
2.延迟加载代理:
// 目标接口
class Image {
display() {
throw new Error("This method must be overridden!");
}
}
// 目标对象
class RealImage extends Image {
constructor(filename) {
super();
this.filename = filename;
this.loadFromDisk();
}
loadFromDisk() {
console.log(`Loading image: ${this.filename}`);
}
display() {
console.log(`Displaying image: ${this.filename}`);
}
}
// 代理对象
class ProxyImage extends Image {
constructor(filename) {
super();
this.filename = filename;
this.realImage = null; // 延迟初始化
}
display() {
if (this.realImage === null) {
this.realImage = new RealImage(this.filename); // 仅在需要时创建目标对象
}
this.realImage.display();
}
}
// 客户端
const image = new ProxyImage("test_image.jpg");
// 第一次访问会加载图像
image.display(); // 输出: Loading image: test_image.jpg \n Displaying image: test_image.jpg
// 第二次访问直接显示图像
image.display(); // 输出: Displaying image: test_image.jpg
3.访问控制代理
// 目标接口
class Database {
query(sql) {
throw new Error("This method must be overridden!");
}
}
// 目标对象
class RealDatabase extends Database {
query(sql) {
console.log(`Executing query: ${sql}`);
return "Query result";
}
}
// 代理对象
class ProtectedDatabase extends Database {
constructor() {
super();
this.realDatabase = new RealDatabase();
this.accessGranted = false;
}
authenticate(password) {
this.accessGranted = password === "secret";
}
query(sql) {
if (!this.accessGranted) {
throw new Error("Access denied! Please authenticate first.");
}
return this.realDatabase.query(sql);
}
}
// 客户端
const db = new ProtectedDatabase();
// 未认证时尝试查询
try {
db.query("SELECT * FROM users");
} catch (e) {
console.log(e.message); // 输出: Access denied! Please authenticate first.
}
// 认证后查询
db.authenticate("secret");
console.log(db.query("SELECT * FROM users")); // 输出: Executing query: SELECT * FROM users \n Query result
4.ES6 Proxy
let person = {
name:"张三",
age:20,
phone:'187xxxxxxxx'
}
let agent = new Proxy(person,{
get(target,key){
if(key === 'phone'){
return '159xxxxxxxx'
}else if(key === 'name'){
return '李四'
}else if(key === 'address'){
return '北京市'
}else if(key === 'customPrice'){
return target.customPrice
}
},
set(target,key,value){
if(key === 'customPrice'){
if(value > 1000){
throw new Error('价格不能超过1000')
}else{
target.customPrice = value
}
}
}
})
console.log(agent.name); // 李四
console.log(agent.phone); // 159xxxxxxxx
console.log(agent.address); // 北京市
agent.customPrice = 2000;
console.log(agent.customPrice); // 抛出错误: 价格不能超过1000
优点:
-
职责分离:代理模式将客户端与目标对象解耦,可以在代理中添加额外的逻辑而不影响目标对象。
-
延迟加载:代理可以延迟目标对象的创建或初始化,提高性能。
-
访问控制:代理可以控制对目标对象的访问,增加安全性。
-
开闭原则:可以再不修改目标对象的情况下扩展功能。
缺点:
-
复杂性:引入代理会增加系统的复杂性,尤其是在多层代理的情况下。
-
性能开销:代理模式可能会引入额外的调用层次,导致一定的性能开销。
常见应用:
-
虚拟代理:用于延迟加载大资源(如图片、文件)。
-
保护代理:用于控制对敏感对象的访问。
-
缓存代理:用于缓存目标对象的结果。
-
远程代理:用于处理远程对象的访问(如RPC调用)。
总结: 代理模式是一种强大的设计模式,适用于需要在访问目标对象时添加额外逻辑的场景。它通过代理对象间接访问目标对象,实现了职责分离和功能扩展,同时也保持了系统灵活性和可维护性。
3.行为型模式
3.1 策略模式
概念:策略模式是一种行为型设计模式,允许在运行时选择算法的行为。策略模式通过定义一系列算法,并将每个算法封装在独立的类中,使得它们可以相互替换。这样,客户端可以根据需要选择不同的算法,而不需要修改代码。
核心思想:
策略模式的核心是将算法的定义与使用分离。它通过将算法封装在独立的策略类中,使得客户端可以再运行时动态切换算法,而不需要修改客户端代码。
使用场景:
-
多种算法实现:当一个系统需要在多种算法之间动态切换时。
-
避免条件语句:当需要避免使用大量的条件语句来选择不同的算法时。
-
算法复用:当多个类具有相似的算法,但具体实现不同时。
-
扩展性:当需要方便的扩展新的算法时。
结构:
-
策略接口(Strategy):定义所有支持算法的公共接口。
-
具体策略(Concrete Strategt):实现策略接口的具体算法类。
-
上下文(Context):持有一个策略对象的引用,并通过策略接口调用具体算法。
代码实现:
1.支付策略
假设我们有一个支付系统,支持多种支付方式(如信用卡、支付宝、微信支付)。我们可以使用策略模式来实现这一需求。
// 策略接口
class PaymentStrategy {
pay(amount) {
throw new Error("This method must be overridden!");
}
}
// 具体策略:信用卡支付
class CreditCardPayment extends PaymentStrategy {
pay(amount) {
console.log(`Paid ${amount} using Credit Card`);
}
}
// 具体策略:支付宝支付
class AlipayPayment extends PaymentStrategy {
pay(amount) {
console.log(`Paid ${amount} using Alipay`);
}
}
// 具体策略:微信支付
class WechatPayment extends PaymentStrategy {
pay(amount) {
console.log(`Paid ${amount} using Wechat Pay`);
}
}
// 上下文
class PaymentContext {
constructor(strategy) {
this.strategy = strategy;
}
setStrategy(strategy) {
this.strategy = strategy;
}
executePayment(amount) {
this.strategy.pay(amount);
}
}
// 客户端
const paymentContext = new PaymentContext(new CreditCardPayment());
paymentContext.executePayment(100); // 输出: Paid 100 using Credit Card
// 切换策略
paymentContext.setStrategy(new AlipayPayment());
paymentContext.executePayment(200); // 输出: Paid 200 using Alipay
// 切换策略
paymentContext.setStrategy(new WechatPayment());
paymentContext.executePayment(300); // 输出: Paid 300 using Wechat Pay
优点:
-
灵活性:可以再运行时动态切换算法,而不需要修改客户端代码。
-
可扩展性:新增算法时,只需要添加新的策略类,符合开闭原则。
-
职责分离:将算法的定义与使用分离,使得代码清晰更易于维护。
缺点:
-
类数量增加:每个具体的策略都需要一个单独的类,可能会导致类的数量增加。
-
客户端需要了解策略:客户端要知道不同策略的区别,以便选择合适的策略。
常见应用:
-
支付系统:支持多种支付方式(如信用卡、支付宝、微信支付)。
-
排序算法:支持多种排序算法(如快速排序、地柜排序、冒泡排序)。
-
压缩算法:支持多种压缩算法(如zip、rar、7z)。
-
导航系统:支持多种路径规划算法(如最短路径、最快路径、最少收费路径)。
总结: 策略模式是一种非常实用的设计模式,特别适用于需要在运行时动态切换算法的使用场景。它通过将算法封装在独立的策略类中,使得系统更加灵活、可扩展,避免了大量的条件语句。策略模式的核心思想是将算法的定义与使用分离,符合面向对象设计的原则。
3.2 观察者模式
概念:观察者模式又称为发布订阅模式(Publish/Subcribe),它定义了一种一对一或一对多的关系,让多个观察者对象同时监听一个主题对象,这个主题对象的状态发生变化时就会通知所有的观察者对象,使得它们能够自动更新,典型代表Vue/React等。
核心思想:
-
主题(subject): 维护一个观察者列表。
提供注册(register)和移除(remove)观察者的方法。
提供通知(notify)方法,用于在状态变化时通知所有观察者。
-
观察者(observer):
定义一个更新接口(如update方法),用于接收主题的通知。
实现步骤:
-
定义观察者接口:所有观察者必须实现该接口,以便主题能够统一通知。
-
定义主题类:维护一个观察者列表。实现注册、移除和通知方法。
-
实现具体观察者:实现观察者接口,定义接收到通知后的行为。
代码实现:
// 被观察者
function Subject(){
this.observers = [];
}
Subject.prototype = {
// 订阅
subscribe: function(observer){
this.observers.push(observer);
},
// 取消订阅
unsubscribe: function(observer){
this.observers = this.observers.filter(item => item !== observer);
},
// 通知
notify: function(){
this.observers.forEach(observer => observer.update('你好呀'));
}
}
// 观察者
function Observer(name){
this.name = name;
this.update = function(val){
console.log(this.name + '收到通知:' + val );
}
}
// 客户端
const subject = new Subject();
const observer1 = new Observer('Observer1');
const observer2 = new Observer('Observer2');
subject.subscribe(observer1);
subject.subscribe(observer2);
subject.notify();
subject.unsubscribe(observer1);
subject.notify();
// Observer1收到通知
// Observer2收到通知
优点:
-
解耦:主题和观察者之间松耦合,易于扩展和维护。
-
动态关系:可以在运行时动态添加或移除观察者。
缺点:
-
性能问题:如果观察者过多,通知所有观察者可能会影响性能。
-
循环依赖:不当使用可能导致循环依赖,引发复杂性问题。
使用场景:
-
当一个对象的改变需要同时引起其他对象改变时。
-
当一个对象需要通知其他对象,但又不希望与被通知对象紧密耦合。
前端中应用:
-
事件处理:DOM事件监听就是观察者模式典型应用。
-
状态管理:如Redux或Vue中的状态变化通知机制。
-
数据绑定:如Vue或Angular中的数据绑定机制。
总结: 通过观察者模式,前端开发可以更灵活地处理对象间的依赖关系,提升代码的可维护性和扩展性。
3.3 迭代器模式
概念:迭代器模式(Iterator pattern)是一种行为设计模式,它提供了一种顺序访问聚合对象(如列表、集合等)中元素的方法,而无需暴露其底层表示。通过使用迭代器,可以在不关心聚合对象内部结构的情况下遍历其元素。
核心思想:
-
迭代器(Iterator): 定义访问和遍历元素的接口,通常包括next()和hasNext()方法。 next()返回当前元素并移动到下一个元素。
hasNext()检查是否还有更多元素可以遍历。
-
聚合对象(Aggregate): 定义创建迭代器的接口 ,通常是一个createIterator()方法。 聚合对象可以是数组、列表、树等数据结构。
实现步骤:
-
定义迭代器接口:包含next()和hasNext()方法。
-
实现具体迭代器:实现迭代器接口,提供具体遍历逻辑。
-
定义聚合对象接口:包含createIterator()方法。
-
实现具体聚合对象:实现聚合对象接口,返回一个具体的迭代器实例。
代码实现:
1.简单代码实现
const items = [1,2,3,4,5]
// 定义迭代器接口
function Iterator(items) {
this.items = items
this.index = 0
}
// 定义迭代器方法
Iterator.prototype = {
hasNext() {
return this.index < this.items.length
},
next() {
return this.items[this.index++]
}
}
const iterator = new Iterator(items)
while(iterator.hasNext()) {
console.log(iterator.next())
}
// 1 2 3 4 5
2.ES6代码实现(for...of)
ES6提供了更简单的迭代循环语法 for...of
,使用该语法的前提是操作对象需要实现 可迭代协议(The iterable protocol),简单说就是该对象有个Key为 Symbol.iterator
的方法,该方法返回一个iterator对象。
function Range(start, end){
return {
[Symbol.iterator]: function(){
return{
next(){
if(start < end ){
return {value: start++, done: false}
}
return {done: true, value: end}
}
}
}
}
}
for(let item of Range(1, 5)){
console.log(item)
}
优点:
-
简化遍历:提供统一的接口遍历不同类型的聚合对象。
-
隐藏实现细节:客户端无需了解聚合对象内部结构。
-
支持多种遍历方式:可以为同一个聚合对象提供多种遍历方式。
缺点:
-
复杂性增加:对于简单的聚合对象,使用迭代器可能会增加代码复杂性。
-
性能开销:迭代器可能会引入额外的性能开销。
使用场景:
-
需要遍历复杂数据结构(如树、图)时。
-
需要提供多种遍历方式时。
-
希望隐藏聚合对象的内部实现细节。
前端中的应用:
-
遍历DOM元素:可以使用迭代器模式遍历DOM树中的元素。
-
处理集合数据:如遍历数组、列表等数据结构。
-
自定义数据结构:如实现自定义的树或图结构,并提供遍历接口。
总结: 通过迭代器模式,前端开发可以更灵活地处理各种数据结构的遍历需求,提升代码的可维护性和扩展性。
3.4 状态模式
概念:状态模式(State Pattern)是一种行为设计模式,它允许对象在其内部状态改变时改变其行为。状态模式将对象的行为封装在不同的状态类中,使得对象在不同状态下有不同的行为表现,并且可以在运行时动态切换状态。
核心思想:
-
上下文(Context): 维护一个当前状态的引用。 提供一个接口,用于客户端与状态对象交互。 可以将状态相关的行为委托给当前状态对象。
-
状态接口(State Interface): 定义所有具体状态类必须实现的方法。 这些方法通常对应于上下文对象在不同状态下的行为。
-
具体状态类(Concrete States): 实现状态接口,定义在特定的状态下的行为。 每个具体状态类负责处理上下文在该状态下的行为。
实现步骤:
-
定义状态接口:包含所有状态类必须实现的方法。
-
实现具体状态类:每个具体状态类实现状态接口,定义在该状态下的行为。
-
定义上下文类:维护一个当前状态的引用,提供一个接口,用于客户端与状态对象交互。可以将状态相关的行为委托给当前状态对象。
代码实现:
红绿灯案例
// 状态(红灯、绿灯、黄灯)
class State{
constructor(color){
this.color = color;
}
// 设置状态
handle(context){
console.log(`现在变成${this.color}灯`);
context.setState(this)
}
}
// 上下文
class Context{
constructor(){
this.state = null;
}
// 获取状态
getState(){
return this.state;
}
// 设置状态
setState(state){
this.state = state;
}
}
// 测试
const context = new Context();
const red = new State('红灯');
const green = new State('绿灯');
const yellow = new State('黄灯');
red.handle(context);
green.handle(context);
yellow.handle(context);
console.log(context.getState().color);
// 现在变成红灯
// 现在变成绿灯
// 现在变成黄灯
优点:
-
单一职责原则:将不同状态的行为分离到不同的类中,符合单一职责原则。
-
开闭原则:易于添加新的状态类,无需修改现有代码。
-
简化上下文代码:上下文类无需包含大量的条件语句来管理状态。
缺点:
-
复杂性增加:对于简单的状态机,使用状态模式可能会增加代码复杂性。
-
状态类数量增多:如果状态较多,可能会导致类的数量增加。
使用场景:
-
对象的行为依赖于其状态,并且需要在运行时根据状态改变行为。
-
代码中包含大量与状态相关的条件语句时。
-
需要动态切换对象的状态时。
前端中的应用:
-
UI组件状态管理:如按钮在不同状态下的行为(禁用、激活、悬停等)。
-
表单验证:根据表单字段的不同状态(有效、无效、正在验证等)执行不同的验证逻辑。
-
游戏开发:管理游戏角色的不同状态(行走、奔跑、跳跃等)。