Skip to main content

概览

在高级前端面试中,如果只停留在“名词解释”阶段(比如“我知道单例模式就是全局只有一个实例”),是很难拿到高分的。面试官更看重的是:你在实际业务中,如何利用这些模式来解决代码耦合、难以扩展的问题?

以下是前端最常用的几种设计模式,以及它们在经典开源库中的体现实际项目中的应用场景(可以直接拿来在面试中分享)


1. 单例模式 (Singleton)

核心思想:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

  • 开源库体现
    • Redux / Vuex / Pinia:全局的 Store 就是一个典型的单例。无论你在哪个组件里 useSelector,拿到的都是同一份数据来源。
    • Axios 的默认实例:我们平时直接使用的 axios.get(),其实就是 Axios 内部默认创建的一个单例对象。
  • 面试分享场景:全局弹窗/提示组件 (Toast/Modal)

    “在我们项目中,全局的 Toast 提示组件我采用了单例模式。因为如果每次调用 Toast.show() 都在 DOM 里 append 一个新的 div 节点,连续报错时页面上就会堆叠无数个无用的 DOM,非常消耗性能。所以我通过闭包缓存了 Toast 的实例,如果实例存在就直接复用它并改变文案,不存在才会去创建真正的 DOM 节点。”

2. 策略模式 (Strategy)

核心思想:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。目的是为了消除代码里大片的 if-elseswitch-case

  • 开源库体现
    • 表单校验库 (Async-Validator):Element/Antd 底层使用的校验库,里面各种 type: 'string', type: 'email' 其实就是映射到了不同的校验策略函数上。
  • 面试分享场景:多场景的表单校验 / 多种支付方式的分发

    “在我们业务中有个充值收银台,根据用户的选择有‘微信’、‘支付宝’、‘银联’、‘苹果内购’等多种支付逻辑。以前的代码是在一个巨大的函数里写了几百行的 if-else,一旦新增支付方式很容易改出 bug。后来我用策略模式重构了它:定义了一个 PayStrategy 对象(或者 Map),把每种支付方式的底层拉起逻辑抽离成独立的策略函数挂载在对象上。主函数只需要执行 PayStrategy[payType]() 即可,代码不仅符合了开闭原则(对扩展开放,对修改关闭),而且变得非常清爽。”

3. 观察者模式 vs 发布-订阅模式 (Observer vs Pub-Sub)

核心思想:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

面试必考:观察者模式和发布-订阅模式的区别是什么?

  • 观察者模式:观察者(Observer)和目标对象(Subject)是直接通信的。比如 Vue2 的响应式原理,Watcher(观察者)直接订阅了 Dep(目标对象)。
  • 发布-订阅模式:发布者和订阅者之间互不认识,它们必须通过一个第三方中介(Event Bus / 调度中心)来通信。
  • 开源库体现
    • 观察者:DOM 的 addEventListener
    • 发布-订阅:Node.js 的 EventEmitter,Vue 中的 EventBus ($on/$emit)
  • 面试分享场景:跨组件/跨 iframe 通信

    “在开发一个低代码可视化编辑器时,左侧是物料区,中间是画布,右侧是属性配置面板。它们之间隔得很远(甚至在不同的 iframe 里),如果用 Props 传递层级太深,用 Redux 又嫌太重。所以我手写了一个轻量级的 EventBus(发布订阅模式),左侧拖拽物料时触发 emit('onDragStart'),画布区 on('onDragStart') 做出高亮响应,实现了完全解耦的通信。”

4. 代理模式 (Proxy)

核心思想:为一个对象提供一个代用品或占位符,以便控制对它的访问。

💡 为什么代理模式如此重要?(面试官最爱听的理解) 代理模式的灵魂在于“无侵入式增强”(符合开闭原则 OCP)。当你不方便、或者不能直接修改一个本体对象(比如它是第三方库的方法、或者是一个十分复杂的旧逻辑),但你又想在它执行前后加上一些额外的逻辑(如:权限校验、缓存记录、日志打印、延迟加载)时,代理模式是唯一的解法。

  • 开源库与底层机制体现

    • Vue3 响应式原理:这是前端最经典的代理应用。Vue3 使用 ES6 Proxy 代理了原始的普通对象。当你读取数据时,代理对象偷偷做了“依赖收集”;当你修改数据时,代理对象偷偷去触发“视图更新”。原始对象根本不知道自己被监听了。
    • 事件委托:利用事件冒泡,父节点充当了所有子节点的“代理”,子节点的点击事件全由父节点代为处理并分发,极大地节省了内存。
    • Nginx 反向代理:前端本地开发时的 webpack-dev-server proxy 也是代理模式在架构上的体现。
  • 面试分享场景:虚拟代理(图片懒加载)与缓存代理

    场景 1(虚拟代理):图片懒加载 “如果直接让 <img> 标签加载一张高清大图,网络慢时会留下一大片空白。我可以写一个虚拟代理对象,它对外暴露和真实 Image 一样的 setSrc 接口。在这个代理内部,先让页面上的 <img> 显示一张本地的 loading 占位图,然后在后台偷偷 new 一个真正的 Image 去下载高清图,等下载完了,代理对象再把真实链接替换到页面的 <img> 上。在这个过程中,真实 Image 对象只负责最纯粹的图片下载,而代理对象负责处理 Loading 逻辑,完美解耦。”

    场景 2(缓存代理):高频接口请求拦截 “业务中有个拉取省市区级联数据的接口被频繁调用。我没有直接去改原来的业务请求函数,而是写了一个代理函数 proxyGetCityData,它内部维护了一个 Map 闭包。每次发起请求前先查 Map 里有没有,有就直接返回缓存,没有才调用本体函数去发 HTTP 请求。这样既实现了缓存,又没有污染本体函数的纯粹逻辑。”

5. 适配器模式 (Adapter)

核心思想:解决两个软件实体间的接口不兼容的问题。把一个类的接口转换成客户希望的另外一个接口。

  • 开源库体现
    • Axios 的底层适配:Axios 之所以既能在浏览器发请求,又能在 Node.js 发请求,是因为它底层实现了一个适配器:根据环境自动选择是调用浏览器的 XMLHttpRequest 还是 Node.js 的 http 模块,但暴露给开发者的 API 都是一致的 axios.get
  • 面试分享场景:旧接口数据格式的平滑过渡

    “我们项目在做后端架构重构时,新接口返回的数据结构(比如树形结构变成了扁平数组)跟老接口完全不一样了。为了不修改前端成百上千个业务组件里的渲染逻辑,我写了一个数据适配器函数。在拿到新接口数据后,先经过适配器将其转换成老接口的数据格式再传给 UI 层。这样用极低的成本实现了业务的平滑过渡。”

6. 装饰者模式 (Decorator)

核心思想:在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责。

  • 开源库体现
    • React 高阶组件 (HOC):如 Redux 的 connectwithRouter
    • ES6 装饰器:MobX 的 @observable,NestJS 的 @Controller
  • 面试分享场景:无侵入的埋点与防抖

    “在我们做前端埋点时,如果要给几十个按钮加上点击上报逻辑,直接改原有的 onClick 函数侵入性太强了。所以我写了一个 @track 的装饰器(或者高阶函数),把它包在原来的点击函数外面。在执行原本业务逻辑之前,装饰器会自动先发送一条埋点日志。同理,我还用装饰者模式封装过 @debounce 防抖装饰器,极大提高了代码的复用率。”

7. 插件模式 / 微内核架构 (Plugin Pattern)

核心思想:核心系统(Core)只负责最基础的调度和生命周期管理,把所有复杂的业务功能剥离成一个个独立的插件(Plugin)。插件通过钩子(Hooks)注入到核心系统中。

  • 开源库体现
    • Webpack (Tapable):Webpack 核心只是一个极简的事件流流转引擎,所有的编译、压缩、代码分割全都是通过 Plugin 实现的。
    • Vue / ViteVue.use(plugin) 机制,以及 Vite 基于 Rollup 的插件生态。
  • 面试分享场景:高可扩展的 SDK 或编辑器

    “我在负责公司内部的一个富文本编辑器(或视频播放器) SDK 时,采用了微内核插件架构。核心包只保留了最基础的文本输入和光标选区管理。至于‘加粗’、‘插入图片’、‘@人’这些功能,全部设计成独立的 Plugin。业务方在接入时,可以通过配置按需引入插件,既减小了 SDK 的打包体积,又保证了极高的可扩展性,别人想加新功能完全不需要动我的核心源码。”

8. 责任链模式 (Chain of Responsibility)

核心思想:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。

在现代前端中,它通常演化为大名鼎鼎的“中间件模式(Middleware)”或“洋葱模型”

  • 开源库体现
    • Axios 拦截器 (Interceptors):请求发出去之前,会经过一个拦截器链(加 Token -> 格式化参数);响应回来时,又经过一个拦截器链(处理状态码 -> 解构数据)。
    • Koa 洋葱模型 / Redux Middlewares:经典的中间件链式调用。
  • 面试分享场景:复杂的表单校验或请求管道

    “我利用责任链模式重构了前端的网络请求底层模块。以前的请求函数里堆满了各种 if-else(判断是否断网、判断是否 401 登录失效、判断业务错误码)。重构后,我把每个逻辑抽离成一个独立的中间件处理器,组装成一条责任链。每次发请求,数据在这条链上流转,任何一个节点处理异常就可以直接阻断并抛出,代码变得极其清晰且易于测试。”

9. 工厂模式 (Factory)

核心思想:将对象的创建逻辑封装起来,对外提供一个统一的接口。使用者不需要关心内部是怎么 new 的。

  • 开源库体现
    • React / Vue 的虚拟 DOMReact.createElement(type, props, children) 或者 Vue 的 h() 函数,本质上就是一个庞大的工厂函数,根据传入的 type 返回不同类型的 VNode 对象。
  • 面试分享场景:通用弹窗/组件调用封装

    “业务里有很多种不同风格的弹窗(Success、Warning、Error)。我没有在每个页面里去写长长的 JSX,而是封装了一个 ModalFactory 类。只需要调用 ModalFactory.create('success', { text: '成功' }),工厂内部会自动帮我组装好对应的组件并挂载到 body 上,极大简化了业务侧的调用心智。”