先记住这个答案
JavaScript的一等函数让策略模式常可退化为函数参数,省去策略类样板。当策略仅是一个算法且无额外状态时,直接传函数更简洁。但当策略需要维护内部状态、需要命名空间或需与策略模式其他特性(如运行时替换、组合)结合时,定义策略类仍必要。取舍关键看策略的复杂度和变化维度。
- 单个无状态算法适合直接传函数
- 策略需要状态或命名空间时用类
- 判断标准是变化维度和上下文需求
函数参数如何取代策略类
传统策略模式在Java等语言中要求定义策略接口和多个实现类,客户端依赖接口。JavaScript函数是一等公民,一个函数既能携带行为也能被传递,因此一个函数参数就相当于一个隐式策略接口。例如比较器可传函数而非Comparator对象。
但函数作为策略只暴露单一入口,若策略需要多个方法或可配置状态,函数式表达需依赖闭包,而闭包封装的状态与函数一起传递。当策略需要公开展示其属性和方法时,类结构更明确。机制不同导致取舍:函数式更轻,类式更结构化。
折扣计算:何时该用函数还是类
场景是电商价格计算,支持多种折扣策略如无折扣、固定折扣、满减。直接传函数:new PriceCalculator(product => product.price * 0.9) 简洁。但另一种场景是折扣策略需要记录调用次数、有效期等状态,且需要校验配置,此时函数闭包维护的状态不直观。
我采用混合:纯计算规则用箭头函数传递,有状态或需要多方法(如isApplicable和calculate)的折扣实现为策略类实例。这样保持调用处的简洁,同时尊重复杂策略的封装。实践中评估策略是否独立可命名和可能演进。
// 直接传递函数(无状态)
function applyDiscount(price, discountFn) {
return discountFn(price);
}
const tenPercent = p => p * 0.9;
console.log(applyDiscount(100, tenPercent)); // 90
// 策略类需要维护状态
class VipStrategy {
constructor() {
this.calls = 0;
}
apply(price) {
this.calls++;
return price * 0.8;
}
getCalls() { return this.calls; }
}
const vip = new VipStrategy();
console.log(vip.apply(100), vip.getCalls()); // 80 1查看输出与解释
90
80 1演示函数参数适合单次计算,而策略类携带状态。注意VipStrategy实例方法被调用后状态变化。
失效条件:函数式策略的局限
直接传函数失效于策略需要多个协作方法时。例如一个支付策略需要validate、pay和refund,只传一个函数会迫使各逻辑分散。另外,函数没有显式类型,在TypeScript中用函数接口仍可能需要类型断言,而类可提供反射或元数据。
如果策略集可能变化且希望运行时动态增删,类是更好的容器。但过度设计也会出现:为一个简单回调创建类层次。判断准则:策略是否只有一个关注点?是否无需保存状态?若是,函数即可;否则考虑类。当上下文需要统一调用接口而函数签名不统一,可借助适配器函数。
容易答错的地方
- 认为函数能完全替代策略类
- 错误。当策略需要多个操作或内部状态时,单个函数无法承担,需用对象。JS中函数本身可用属性,但这会污染函数语义。正确评估:单方法策略可用函数;多方法或状态用类。
- 盲目为所有策略建立类结构
- 过度使用类会引入样板和冗长。例如只有一行逻辑的验证器写成类实例导致代码碎片化。应优先考虑函数参数,仅在策略逻辑足够复杂或需要独立测试多个辅助方法时才引入类。
面试官还会怎么问?
TypeScript中直接传函数与策略类的类型差异?
TS中用函数类型可精确描述,如(price:number)=>number。策略类需定义接口,但能统一接口并支持同名方法。若策略函数参数和返回值不同,需联合类型或泛型。类更易实现函数重载和私有方法。
何时应该从函数升级为策略类?
当函数的闭包状态增多、函数长度超过一个屏幕且需要多阶段处理时。另一个信号:多个函数共享一组私有逻辑,可提取为类的内联私有方法。升级也发生在需要生命周期钩子(如初始化、销毁)时。
策略模式中直接传函数是否影响测试?
不影响,纯函数更易测,用桩断言调用即可。策略类则需实例化并验证状态。但若策略函数引用外部变量,测试需控制环境。函数式策略更简单但难以测试其内部行为,可导出纯函数辅助测试。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。