在上篇内容中,我们介绍了单例、工厂、观察者三种常用JS设计模式,聚焦于"对象创建"和"对象间通信"的核心场景。本篇将继续讲解另外三种高频设计模式------策略模式、装饰器模式、代理模式,它们分别解决"算法灵活切换""功能动态扩展""对象访问控制"的问题,是JS开发中提升代码可维护性、可扩展性的关键技巧,尤其适配前端组件开发、业务逻辑封装等实际场景。
一、策略模式(Strategy Pattern)
1. 核心思想
定义一系列算法(策略),将每个算法封装成独立的对象,使它们可以相互替换,且算法的变化不会影响使用算法的客户端。核心是"分离算法的定义与使用",避免大量if-else或switch-case判断,让代码更具灵活性和可维护性。
简单理解:就像去餐厅吃饭,"支付"是一个核心行为,而"微信支付""支付宝支付""现金支付"就是不同的策略,我们可以根据需求切换支付策略,无需修改"支付"的核心逻辑。
2. JS 实现(实战场景:表单验证)
表单验证是前端高频场景,不同字段(手机号、邮箱、密码)的验证规则不同,若用if-else判断会导致代码臃肿,用策略模式可完美解决。
javascript
// 1. 定义策略对象(封装不同的验证算法)
const validateStrategies = {
// 非空验证
required: (value, msg) => {
if (!value.trim()) return msg;
},
// 手机号验证
phone: (value, msg) => {
const reg = /^1[3-9]\d{9}$/;
if (!reg.test(value)) return msg;
},
// 邮箱验证
email: (value, msg) => {
const reg = /^[\w-]+@[a-zA-Z0-9]+.[a-zA-Z]{2,4}$/;
if (!reg.test(value)) return msg;
},
// 密码长度验证
minLength: (value, msg, length) => {
if (value.length < length) return msg;
}
};
// 2. 定义上下文(使用策略的客户端)
class Validator {
constructor() {
this.strategies = []; // 存储当前需要执行的策略
}
// 添加验证规则(往策略列表中添加策略)
add(value, strategyName, msg, ...params) {
this.strategies.push(() => {
// 执行对应的策略,返回错误信息
return validateStrategies[strategyName](value, msg, ...params);
});
}
// 执行所有验证策略,返回第一个错误信息
validate() {
for (const strategy of this.strategies) {
const errorMsg = strategy();
if (errorMsg) return errorMsg; // 有错误直接返回
}
return null; // 无错误返回null
}
}
// 3. 实战使用
const validator = new Validator();
// 给手机号添加验证规则
validator.add('123456', 'required', '手机号不能为空');
validator.add('123456', 'phone', '手机号格式错误');
// 给密码添加验证规则
validator.add('123', 'required', '密码不能为空');
validator.add('123', 'minLength', '密码长度不能少于6位', 6);
// 执行验证
const error = validator.validate();
console.log(error); // 输出:手机号格式错误
3. 应用场景
- 表单验证(不同字段的验证规则切换);
- 排序算法切换(冒泡排序、快速排序、插入排序按需切换);
- 支付方式、登录方式切换(微信、支付宝、QQ登录等);
- 前端主题切换(浅色、深色、自定义主题)。
4. 优缺点
优点:算法可独立封装、灵活切换,避免冗余判断;代码扩展性强,新增策略无需修改原有逻辑(符合开闭原则);策略可复用。
缺点:策略数量过多时,会增加策略对象的维护成本;客户端需要了解所有策略的存在,才能选择合适的策略。
二、装饰器模式(Decorator Pattern)
1. 核心思想
动态地给一个对象添加额外的职责(功能),而不改变其原有的结构和核心逻辑。装饰器是一种"包装器",它包裹着原对象,在不破坏原对象的前提下,扩展其功能。
简单理解:就像给手机贴钢化膜、戴手机壳------手机的核心功能(通话、上网)不变,但新增了"防刮""防摔"的额外功能;装饰器就是"手机壳",原对象就是"手机"。
注意:JS中的装饰器(ES7提案)与传统装饰器模式原理一致,但语法更简洁,目前需通过Babel转译才能兼容低版本浏览器。
2. JS 实现(两种方式:传统方式 + ES7装饰器)
方式1:传统方式(手动封装装饰器)
javascript
// 1. 原对象(核心功能)
class Coffee {
// 核心方法:制作咖啡
make() {
console.log('制作一杯纯咖啡');
return '纯咖啡';
}
}
// 2. 装饰器1:加牛奶(扩展功能)
function addMilk(coffee) {
// 保存原方法
const originalMake = coffee.make;
// 重写make方法,添加额外功能
coffee.make = function() {
const result = originalMake.call(this); // 执行原核心逻辑
console.log('添加牛奶');
return `${result} + 牛奶`;
};
return coffee;
}
// 3. 装饰器2:加糖浆(扩展功能)
function addSyrup(coffee) {
const originalMake = coffee.make;
coffee.make = function() {
const result = originalMake.call(this);
console.log('添加糖浆');
return `${result} + 糖浆`;
};
return coffee;
}
// 4. 使用装饰器(动态扩展功能)
let myCoffee = new Coffee();
myCoffee = addMilk(myCoffee); // 给咖啡加牛奶
myCoffee = addSyrup(myCoffee); // 给咖啡加糖浆
myCoffee.make();
// 输出:
// 制作一杯纯咖啡
// 添加牛奶
// 添加糖浆
方式2:ES7装饰器(语法糖,更简洁)
javascript
// 1. 定义装饰器函数(类装饰器)
function addMilk(target) {
// target 是被装饰的类(Coffee)
const originalMake = target.prototype.make;
target.prototype.make = function() {
const result = originalMake.call(this);
console.log('添加牛奶');
return `${result} + 牛奶`;
};
}
// 2. 定义装饰器函数(可传参的装饰器)
function addSyrup(syrupType) {
// 返回装饰器函数
return function(target) {
const originalMake = target.prototype.make;
target.prototype.make = function() {
const result = originalMake.call(this);
console.log(`添加${syrupType}糖浆`);
return `${result} + ${syrupType}糖浆`;
};
};
}
// 3. 使用装饰器装饰类
@addMilk // 无参装饰器
@addSyrup('草莓') // 有参装饰器(顺序:从下到上执行,先加糖浆,再加牛奶)
class Coffee {
make() {
console.log('制作一杯纯咖啡');
return '纯咖啡';
}
}
// 4. 使用
const myCoffee = new Coffee();
myCoffee.make();
// 输出:
// 制作一杯纯咖啡
// 添加草莓糖浆
// 添加牛奶
3. 应用场景
- 前端组件功能扩展(如给按钮添加"加载中"状态、防抖节流功能);
- 日志打印、性能监控(在不修改原函数的前提下,新增日志记录、耗时统计);
- 权限控制(给需要权限的方法添加权限校验装饰器);
- React/Vue中的高阶组件(HOC),本质就是装饰器模式的应用。
4. 优缺点
优点:不破坏原对象核心逻辑,动态扩展功能(符合开闭原则);装饰器可叠加使用,灵活性高;代码复用性强。
缺点:过多装饰器会增加代码复杂度,难以调试;ES7装饰器目前仍需转译,存在兼容性问题。
三、代理模式(Proxy Pattern)
1. 核心思想
为一个对象提供一个"代理"(中间层),通过代理对象控制对原对象的访问。代理对象可以在访问原对象前后,执行额外的逻辑(如拦截、过滤、缓存、权限校验),从而保护原对象、增强原对象的功能。
简单理解:就像房产中介------你(客户端)不直接接触房东(原对象),而是通过中介(代理对象)租房,中介可以帮你筛选房源、谈判价格(额外逻辑),同时保护房东的隐私(控制访问)。
JS中最常用的代理实现是 Proxy 对象(ES6新增),它原生支持代理功能,无需手动封装。
2. JS 实现(实战场景:数据拦截、缓存、权限控制)
场景1:数据拦截(监听对象属性的读写)
javascript
// 1. 原对象(目标对象)
const user = {
name: '张三',
age: 20
};
// 2. 创建代理对象(中间层)
const userProxy = new Proxy(user, {
// 拦截属性读取(get方法)
get(target, prop) {
console.log(`读取了${prop}属性,值为:${target[prop]}`);
// 可添加额外逻辑,如默认值
return target[prop] ?? '默认值';
},
// 拦截属性修改(set方法)
set(target, prop, value) {
console.log(`修改了${prop}属性,从${target[prop]}改为${value}`);
// 可添加额外逻辑,如数据校验
if (prop === 'age' && typeof value !== 'number') {
console.error('年龄必须是数字');
return false; // 阻止修改
}
target[prop] = value;
return true; // 允许修改
}
});
// 3. 通过代理对象访问原对象
console.log(userProxy.name); // 读取了name属性,值为:张三 → 张三
userProxy.age = 25; // 修改了age属性,从20改为25
userProxy.age = '26'; // 修改了age属性,从25改为26 → 年龄必须是数字(修改失败)
场景2:缓存代理(减少重复计算)
javascript
// 1. 原函数(耗时计算函数)
function calculate(num) {
console.log('执行了耗时计算');
return num * 10; // 模拟耗时计算
}
// 2. 创建缓存代理
const calculateProxy = new Proxy(calculate, {
cache: {}, // 缓存容器
apply(target, thisArg, args) {
const key = args.join('-'); // 用参数作为缓存key
// 若缓存中存在,直接返回缓存值,无需重复计算
if (this.cache[key]) {
console.log('从缓存中获取结果');
return this.cache[key];
}
// 若缓存中不存在,执行原函数,并存入缓存
const result = target.apply(thisArg, args);
this.cache[key] = result;
return result;
}
});
// 3. 使用代理函数
console.log(calculateProxy(5)); // 执行了耗时计算 → 50
console.log(calculateProxy(5)); // 从缓存中获取结果 → 50(无需重复计算)
console.log(calculateProxy(10)); // 执行了耗时计算 → 100
3. 应用场景
- 数据监听(Vue3响应式原理的核心就是Proxy);
- 缓存代理(减少重复请求、重复计算,提升性能);
- 权限控制(拦截敏感操作,校验用户权限);
- 远程代理(通过代理对象访问远程服务,隐藏远程服务的细节);
- 防抖节流代理(给函数添加防抖节流功能,无需修改原函数)。
4. 优缺点
优点:保护原对象,控制访问权限;可在不修改原对象的前提下,新增额外逻辑(如缓存、拦截);解耦客户端与原对象,提升代码可维护性。
缺点:增加了代理层,可能会轻微影响性能;代理逻辑复杂时,会增加代码调试难度。
四、三种模式对比与总结
| 设计模式 | 核心解决问题 | 核心特点 | 关键区别 |
|---|---|---|---|
| 策略模式 | 算法灵活切换,避免冗余判断 | 封装算法,可相互替换 | 关注"算法本身的切换",客户端需选择策略 |
| 装饰器模式 | 动态扩展对象功能,不破坏原逻辑 | 包装原对象,功能可叠加 | 关注"功能扩展",不改变原对象的访问方式 |
| 代理模式 | 控制对原对象的访问,增强原对象 | 中间层拦截,保护原对象 | 关注"访问控制",客户端通过代理访问原对象 |
总结:三种模式均遵循"开闭原则"(对扩展开放,对修改关闭),是JS中提升代码质量的核心技巧。实际开发中,无需刻意套用模式,而是根据场景选择:
- 需要切换算法/规则 → 策略模式;
- 需要扩展功能且不破坏原逻辑 → 装饰器模式;
- 需要控制访问、添加拦截逻辑 → 代理模式。