前端知库

策略模式和代理模式分别解决什么问题?前端怎样使用?

策略模式把一组可以互换的算法分别封装起来,用一张表按名字选择,替代一长串 if-else,常用于表单校验和价格计算。代理模式在访问对象前加一层,控制、增强或延迟这次访问,比如缓存代理、图片懒加载,以及 Vue 3 用 Proxy 实现的响应式。

参考答案

简答

表单要校验必填、手机号、最小长度,如果全写在一个函数里,就是一长串 if (rule === 'required') ... else if (rule === 'mobile') ...,每加一种规则都得改这个函数。策略模式的做法是把每种校验写成一个独立的函数,放进一个对象里,按规则名取出来调用。新增规则只要往对象里加一项,调用的代码不用改。

代理模式是在使用者和真实对象之间加一层,使用者以为自己在直接用对象,其实每次访问都先经过代理。代理可以在这里做额外的事:缓存结果,避免重复计算或请求;先显示占位图,图片加载好再换上;检查权限;记录访问。JS 的 Proxy 能拦截属性读写,Vue 3 的响应式就是用它在读属性时收集依赖、写属性时触发更新。

两者的区别是关注点不同。策略模式解决的是“同一件事有多种做法,怎么选”;代理模式解决的是“访问一个对象时,怎么在不改它的前提下加控制或增强”。

要注意,规则只有两三种、以后也不会增加时,直接写 if-else 更清楚,不要为了用模式而用模式。

用策略模式写表单校验

JavaScript
const strategies = {
  required: (value) => (value.trim() ? '' : '不能为空'),
  mobile: (value) => (/^1\d{10}$/.test(value) ? '' : '手机号格式不正确'),
  minLength: (value, length) => (value.length >= length ? '' : `至少 ${length} 个字符`),
};

function validate(value, rules) {
  for (const [name, ...args] of rules) {
    const strategy = strategies[name];
    if (!strategy) throw new Error(`未知规则:${name}`);
    const message = strategy(value, ...args);
    if (message) return message;
  }
  return '';
}

console.log(validate('', [['required']])); // 不能为空
console.log(validate('1380000', [['required'], ['mobile']])); // 手机号格式不正确
console.log(validate('ab', [['minLength', 6]])); // 至少 6 个字符
console.log(validate('13800000000', [['required'], ['mobile']]) === ''); // true

validate 本身不知道有哪些规则,它只按名字去 strategies 里找函数调用。所以要加一个“邮箱”规则,只需在 strategies 里加一个 email 函数,validate 一行都不用改。规则写成 [名称, ...参数] 的数组,像 minLength 这种需要参数的规则也能用同一种方式表达。

遇到未知规则名为什么要抛错,而不是忽略?因为拼错规则名时如果静默跳过,校验就悄悄失效了,用户能提交本该被拦住的数据。

用代理模式加缓存

JavaScript
function withCache(fn) {
  const cache = new Map();
  return function (key) {
    if (!cache.has(key)) cache.set(key, fn(key));
    return cache.get(key);
  };
}

const getUser = withCache((id) => fetch(`/api/users/${id}`).then((res) => res.json()));

调用 getUser(1) 的代码和直接调用原函数没有区别,可同一个 id 第二次调用时直接返回缓存里的 Promise,不再发请求。这里缓存的是 Promise,所以两个组件同时请求同一个用户,也只会发出一次请求。实际项目里还要处理请求失败时删除缓存,否则失败的结果会一直被缓存。

用 Proxy 可以代理对象的属性访问,原理和限制见《Proxy 与 Reflect 能实现什么,存在哪些限制?》,Vue 3 怎样用它收集依赖见《Vue 3 响应式依赖是怎样收集和触发的?》。

常见追问

代理模式和装饰器模式有什么区别?

两者都是包一层再调用原对象,结构很像。区别在意图:代理侧重控制访问,比如决定要不要真的调用、什么时候调用;装饰器侧重给对象叠加新功能,原有的调用总会发生。实际讨论中不必纠结名字,说清楚这一层做了什么就行。

前端还有哪些常见的设计模式?

工厂模式用一个函数按参数创建不同的对象,比如根据类型创建不同的图表实例;适配器模式把一个接口转换成另一个,比如把不同后端返回的数据整理成统一格式;装饰器模式比如高阶组件给组件增加功能。