2天时间,聊今年最热的 Agent、上下文工程、AI 产品创新等话题。2025 年最后一场~ 了解详情
写点什么

前端需要掌握的设计模式

  • 2021-07-31
  • 本文字数:7101 字

    阅读完需:约 23 分钟

前端需要掌握的设计模式

提到设计模式,相信知道的同学都会脱口而出,五大基本原则(SOLID)和 23 种设计模式。SOLID 所指的五大基本原则分别是:单一功能原则、开放封闭原则、里式替换原则、接口隔离原则和依赖反转原则。逐字逐句诠释这五大基本原则违背了写这篇文章的初衷,引用社区大佬的理解,SOLID 可以简单概括为六个字,即“高内聚,低耦合”:


  • 层模块不依赖底层模块,即为依赖反转原则。

  • 部修改关闭,外部扩展开放,即为开放封闭原则。

  • 合单一功能,即为单一功能原则。

  • 知识要求,对外接口简单,即为迪米特法则。

  • 合多个接口,不如独立拆分,即为接口隔离原则。

  • 成复用,子类继承可替换父类,即为里式替换原则。


23 种设计模式分为“创建型”、“行为型”和“结构型”。具体类型如下图:



设计模式说白了就是“封装变化”。比如“创建型”封装了创建对象的变化过程,“结构型”将对象之间组合的变化封装,“行为型”则是抽离对象的变化行为。接下来,本文将以“单一功能”和“开放封闭”这两大原则为主线,分别介绍“创建型”、“结构型”和“行为型”中最具代表性的几大设计模式。


创建型

工厂模式


工厂模式根据抽象程度可分为三种,分别为简单工厂、工厂方法和抽象工厂。其核心在于将创建对象的过程封装其他,然后通过同一个接口创建新的对象。 简单工厂模式又叫静态工厂方法,用来创建某一种产品对象的实例,用来创建单一对象。


// 简单工厂class Factory {  constructor (username, pwd, role) {   this.username = username;    this.pwd = pwd;    this.role = role;  }}
class CreateRoleFactory { static create (username, pwd, role) {   return new Factory(username, pwd, role);  }}
const admin = CreateRoleFactory.create('张三', '222', 'admin');
复制代码


在实际工作中,各用户角色所具备的能力是不同的,因此简单工厂是无法满足的,这时候就可以考虑使用工厂方法来代替。工厂方法的本意是将实际创建对象的工作推迟到子类中。


class User { constructor (name, menuAuth) {   if (new.target === User) throw new Error('User 不能被实例化');    this.name = name;    this.menuAuth = menuAuth;  }}
class UserFactory extends User { constructor (...props) { super(...props); } static create (role) { const roleCollection = new Map([ ['admin', () => new UserFactory('管理员', ['首页', '个人中心'])], ['user', () => new UserFactory('普通用户', ['首页'])] ]) return roleCollection.get(role)(); }}
const admin = UserFactory.create('admin');console.log(admin); // {name: "管理员", menuAuth: Array(2)}const user = UserFactory.create('user');console.log(user); // {name: "普通用户", menuAuth: Array(1)}
复制代码


随着业务形态的变化,一个用户可能在多个平台上同时存在,显然工厂方法也不再满足了,这时候就要用到抽象工厂。抽象工厂模式是对类的工厂抽象用来创建产品类簇,不负责创建某一类产品的实例。


class User {  constructor (hospital) {    if (new.target === User) throw new Error('抽象类不能实例化!');    this.hospital = hospital;  }}// 浙一class ZheYiUser extends User {  constructor(name, departmentsAuth) {    super('zheyi_hospital');    this.name = name;    this.departmentsAuth = departmentsAuth;  }}// 萧山医院class XiaoShanUser extends User {  constructor(name, departmentsAuth) {    super('xiaoshan_hospital');    this.name = name;    this.departmentsAuth = departmentsAuth;  }}
const getAbstractUserFactory = (hospital) => { switch (hospital) { case 'zheyi_hospital': return ZheYiUser; break; case 'xiaoshan_hospital': return XiaoShanUser; break; }}
const ZheYiUserClass = getAbstractUserFactory('zheyi_hospital');const XiaoShanUserClass = getAbstractUserFactory('xiaoshan_hospital');
const user1 = new ZheYiUserClass('王医生', ['外科', '骨科', '神经外科']);console.log(user1);const user2 = newXiaoShanUserClass('王医生', ['外科', '骨科']);console.log(user2);
复制代码


小结: 构造函数和创建对象分离,符合开放封闭原则。

使用场景: 比如根据权限生成不同用户。


单例模式


单例模式理解起来比较简单,就是保证一个类只能存在一个实例,并提供一个访问它的全局接口。单例模式又分懒汉式和饿汉式两种,其区别在于懒汉式在调用的时候创建实例,而饿汉式则是在初始化就创建好实例,具体实现如下:


// 懒汉式class Single { static getInstance () {   if (!Single.instance) {     Single.instance = new Single();    }    return Single.instance;  }}
const test1 = Single.getInstance();const test2 = Single.getInstance();
console.log(test1 === test2); // true
复制代码


// 饿汉式class Single { static instance = new Single();
static getInstance () { return Single.instance; }}
const test1 = Single.getInstance();const test2 = Single.getInstance();
console.log(test1 === test2); // true
复制代码

小结: 实例如果存在,直接返回已创建的,符合开放封闭原则。

使用场景: Redux、Vuex 等状态管理工具,还有我们常用的 window 对象、全局缓存等。

原型模式


对于前端来说,原型模式在常见不过了。当新创建的对象和已有对象存在较大共性时,可以通过对象的复制来达到创建新的对象,这就是原型模式。


// Object.create()实现原型模式const user = { name: 'zhangsan',  age: 18};let userOne = Object.create(user);console.log(userOne.__proto__); // {name: "zhangsan", age: 18}

// 原型链继承实现原型模式class User { constructor (name) { this.name = name; } getName () { return this.name; }}
class Admin extends User { constructor (name) { super(name); } setName (_name) { return this.name = _name; }}
const admin = new Admin('zhangsan');console.log(admin.getName());console.log(admin.setName('lisi'));
复制代码

小结: 原型模式最简单的实现方式---Object.create()。

使用场景: 新创建对象和已有对象无较大差别时,可以使用原型模式来减少创建新对象的成本。


结构型

装饰器模式


讲装饰器模式之前,先聊聊高阶函数。高阶函数就是一个函数就可以接收另一个函数作为参数。


const add = (x, y, f) => { return f(x) + f(y);}const num = add(2, -2, Math.abs);console.log(num); // 4
复制代码


函数 add 就是一个简单的高阶函数,而 add 相对于 Math.abs 来说相当于一个装饰器,因此这个例子也可以理解为一个简单的装饰器模式。在 react 中,高阶组件(HOC)也是装饰器模式的一种体现,通常用来不改变原来组件的情况下添加一些属性,达到组件复用的功能。


import React from 'react';
const BgHOC = WrappedComponent => class extends React.Component { render () {   return (     <div style={{ background: 'blue' }}>       <WrappedComponent />      </div>    );  }}
复制代码


小结: 装饰器模式将现有对象和装饰器进行分离,两者独立存在,符合开放封闭原则和单一职责模式。

使用场景: es7 装饰器、vue mixins、core-decorators 等。


适配器模式


适配器别名包装器,其作用是解决两个软件实体间的接口不兼容的问题。以 axios 源码为例:

function getDefaultAdapter() {  var adapter;  // 判断当前是否是 node 环境  if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {    // 如果是 node 环境,调用 node 专属的 http 适配器    adapter = require('./adapters/http');  } else if (typeof XMLHttpRequest !== 'undefined') {    // 如果是浏览器环境,调用基于 xhr 的适配器    adapter = require('./adapters/xhr');  }  return adapter;}
// http adaptermodule.exports = function httpAdapter(config) {  return new Promise(function dispatchHttpRequest(resolvePromise, rejectPromise) {    ...  }}// xhr adaptermodule.exports = function xhrAdapter(config) {  return new Promise(function dispatchXhrRequest(resolve, reject) {    ...  }}
复制代码


其目的就是保证 node 和浏览器环境的入参 config 一致,出参 Promise 都是同一个。

小结: 不改变原有接口的情况下,统一接口、统一入参、统一出参、统一规则,符合开发封闭原则。

使用场景 :拥抱变化,兼容代码。


代理模式

代理模式就是为对象提供一个代理,用来控制对这个对象的访问。在我们业务开发中最常见的有四种代理类型:事件代理,虚拟代理、缓存代理和保护代理。本文主要介绍虚拟代理和缓存代理两类。 提到虚拟代理,其最具代表性的例子就是图片预加载。预加载主要是为了避免网络延迟、或者图片太大引起页面长时间留白的问题。通常的解决方案是先给 img 标签展示一个占位图,然后创建一个 Image 实例,让这个实例的 src 指向真实的目标图片地址,当其真实图片加载完成之后,再将 DOM 上的 img 标签的 src 属性指向真实图片地址。

class ProxyImg { constructor (imgELe) {   this.imgELe = imgELe;    this.DEFAULT_URL = 'xxx';  }  setUrl (targetUrl) {   this.imgEle.src = this.DEFAULT_URL;    const image = new Image();        image.onload = () => {     this.imgEle.src = targetUrl;    }    image.src = targetUrl;  }}
复制代码


缓存代理常用于一些计算量较大的场景。当计算的值已经被出现过的时候,不需要进行第二次重复计算。以传参求和为例:

const countSum = (...arg) => { console.log('count...');  let result = 0;  arg.forEach(v => result += v);  return result;}
const proxyCountSum = (() => { const cache = {}; return (...arg) => { const args = arg.join(','); if (args in cache) return cache[args]; return cache[args] = countSum(...arg); };})()
proxyCountSum(1,2,3,4); // count... 10proxyCountSum(1,2,3,4); // 10
复制代码


小结: 通过修改代理类来增加功能,符合开放封闭模式。

使用场景: 图片预加载、缓存服务器、处理跨域以及拦截器等。


行为型

策略模式


介绍策略模式之前,简单实现一个常见的促销活动规则:


  • 预售活动,全场 9.5 折

  • 大促活动,全场 9 折

  • 返场优惠,全场 8.5 折

  • 限时优惠,全场 8 折


人人喊打的 if-else


const activity = (type, price) => { if (type === 'pre') {   return price * 0.95;  } else if (type === 'onSale') {   return price * 0.9;  } else if (type === 'back') {   return price * 0.85;  } else if (type === 'limit') {   return price * 0.8;  }}
复制代码


以上代码存在肉眼可见的问题:大量 if-else、可扩展性差、违背开放封闭原则等。 我们再使用策略模式优化:


const activity = new Map([ ['pre', (price) => price * 0.95],  ['onSale', (price) => price * 0.9],  ['back', (price) => price * 0.85],  ['limit', (price) => price * 0.8]]);
const getActivityPrice = (type, price) => activity.get(type)(price);
// 新增新手活动activity.set('newcomer', (price) => price * 0.7);
复制代码


小结: 定义一系列算法,将其一一封装起来,并且使它们可相互替换。符合开放封闭原则。

使用场景: 表单验证、存在大量 if-else 场景、各种重构等。

观察者模式


观察者模式又叫发布-订阅模式,其用来定义对象之间的一对多依赖关系,以便当一个对象更改状态时,将通知其所有依赖关系。通过“别名”可以知道,观察者模式具备两个角色,即“发布者”和“订阅者”。正如我们工作中的产品经理就是一个“发布者”,而前后端、测试可以理解为“订阅者”。以产品经理建需求沟通群为例:


// 定义发布者类class Publisher {  constructor () {    this.observers = [];    this.prdState = null;  }  // 增加订阅者  add (observer) {    this.observers.push(observer);  }  // 通知所有订阅者  notify () {    this.observers.forEach((observer) => {      observer.update(this);    })  }  // 该方法用于获取当前的 prdState  getState () {    return this.prdState;  }
// 该方法用于改变 prdState 的值 setState (state) { // prd 的值发生改变 this.prdState = state; // 需求文档变更,立刻通知所有开发者 this.notify(); }}
// 定义订阅者类class Observer { constructor () { this.prdState = {}; } update (publisher) { // 更新需求文档 this.prdState = publisher.getState(); // 调用工作函数 this.work(); } // work 方法,一个专门搬砖的方法 work () { // 获取需求文档 const prd = this.prdState; console.log(prd); }}
// 创建订阅者:前端开发小王const wang = new Observer();// 创建订阅者:后端开发小张const zhang = new Observer();// 创建发布者:产品经理小曾const zeng = new Publisher();// 需求文档const prd = { url: 'xxxxxxx'};// 小曾开始拉人入群zeng.add(wang);zeng.add(zhang);// 小曾发布需求文档并通知所有人zeng.setState(prd);
复制代码


经常使用 Event Bus(Vue) 和 Event Emitter(node)会发现,发布-订阅模式和观察者模式还是存在着细微差别,即所有事件的发布/订阅都不能由发布者和订阅者“私下联系”,需要委托事件中心处理。以 Vue Event Bus 为例:


import Vue from 'vue';
const EventBus = new Vue();Vue.prototype.$bus = EventBus;
// 订阅事件this.$bus.$on('testEvent', func);// 发布/触发事件this.$bus.$emit('testEvent', params);
复制代码


整个过程都是 this.$bus 这个“事件中心”在处理。

小结: 为解耦而生,为事件而生,符合开放封闭原则。

使用场景: 跨层级通信、事件绑定等。


迭代器模式


迭代器模式号称“遍历专家”,它提供一种方法顺序访问一个聚合对象中的各个元素,且不暴露该对象的内部表示。迭代器又分内部迭代器(jquery.each/for...of)和外部迭代器(es6 yield)。 在 es6 之前,直接通过 forEach 遍历 DOM NodeList 和函数的 arguments 对象,都会直接报错,其原因都是因为他们都是类数组对象。对此 jquery 很好的兼容了这一点。 在 es6 中,它约定只要数据类型具备 Symbol.iterator 属性,就可以被 for...of 循环和迭代器的 next 方法遍历。


(function (a, b, c) { const arg = arguments;  const iterator = arg[Symbol.iterator]();    console.log(iterator.next()); // {value: 1, done: false}  console.log(iterator.next()); // {value: 2, done: false}  console.log(iterator.next()); // {value: 3, done: false}  console.log(iterator.next()); // {value: undefined, done: true}})(1, 2, 3)
复制代码


通过 es6 内置生成器 Generator 实现迭代器并没什么难度,这里重点通 es5 实现迭代器:


function iteratorGenerator (list) {  var index = 0;  // len 记录传入集合的长度  var len = list.length;  return {    // 自定义 next 方法    next: funciton () {      // 如果索引还没有超出集合长度,done 为 false      var done = index >= len;      // 如果 done 为 false,则可以继续取值      var value = !done ? list[index++] : undefined;
// 将当前值与遍历是否完毕(done)返回 return { done: done, value: value }; } }}
var iterator = iteratorGenerator([1, 2, 3]);console.log(iterator.next()); // {value: 1, done: false}console.log(iterator.next()); // {value: 2, done: false}console.log(iterator.next()); // {value: 3, done: false}console.log(iterator.next()); // {value: undefined, done: true}
复制代码


小结: 实现统一遍历接口,符合单一功能和开放封闭原则。

使用场景: 有遍历的地方就有迭代器。

写到最后


设计模式的难,在于它的抽象和分散。抽象在于每一设计模式看例子都很好理解,真正使用起来却不知所措;分散则是出现一个场景发现好几种设计模式都能实现。而解决抽象的最好办法就是动手实践,在业务开发中探索使用它们的可能性。本文大致介绍了前端领域常见的 9 种设计模式,相信大家在理解的同时也不难发现,设计模式始终围绕着“封装变化”来提供代码的可读性、扩展性、易维护性。所以当我们工作生活中,始终保持“封装变化”的思想的时候,就已经开始体会到设计模式精髓了。



头图:Unsplash

作者:王君

原文:https://mp.weixin.qq.com/s/0W7yAU9sDkdn-zsaZ9Lv0Q

原文:前端需要掌握的设计模式

来源:微医大前端技术 - 微信公众号 [ID:wed_fed]

转载:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

2021-07-31 18:004246

评论

发布
暂无评论
发现更多内容

企业如何挖掘知识“金矿”?这本白皮书讲得够透彻!

百度大脑

治理有精度,AI赋智加强城市精细化管理

百度大脑

从0到1落地电商小程序之微服务设计

晨亮

「架构实战营」

国家产业政策不断加码,氢能步入加速发展期

易观分析

氢能源 氢能源产业

Apache ShardingSphere 5.1.0 执行引擎性能优化揭秘

SphereEx

数据库 ShardingSphere SphereEx apache 社区

恒源云(GpuShare)_租卡怎么选?看这一篇就够了!

恒源云

人工智能 GPU服务器

Java AOT之GraalVM native image介绍以及简单长连接服务实践

BUG侦探

GraalVM java aot native image

提升客户服务体验的技巧

小炮

客户服务 SaaS平台

详细的网站定制步骤有哪些?

源字节1号

网站开发 软件定制

延期通知 RocketMQ Summit 议题全揭秘

阿里巴巴云原生

弱监督语义分割:从图像级标注快进到像素级预测

网易云信

安全

Kubernetes中API的不同版本, Alpha, Beta, Stable 都是什么?

工程师薛昭君

Kubernetes API

从建好到用好,阿里云原生微服务生态的演进

阿里巴巴云原生

Go 中的空白标识符(下划线)

宇宙之一粟

Go 语言 3月月更

AI+遥感智能解译,赋能智慧城市规划革新

百度大脑

Python 的排序方法 sort 和 sorted 的区别

AlwaysBeta

Python

无监控不运维—浅述各种监控方案使用场景

穿过生命散发芬芳

3月月更

【愚公系列】2022年03月 Docker容器 Kafka集群的搭建

愚公搬代码

3月月更

Windows、Linux、Apple三大操作系统的主流文件系统包含哪些?

Ethereal

错误码设计思考

木小风

Java 架构 错误码

浅谈信息熵在数字体验监控领域的应用

博睿数据

手把手教你从Apk中取出算法

奋飞安全

android 安全 java

TDesign React Starter 发布

TDesign

客户画像赋能百度推广生态实践

百度Geek说

前端 后端

网络安全入门5天速成教程: WEB安全渗透攻防技术

网络安全学海

网络安全 安全 信息安全 渗透测试 WEB安全

ABAP 获取本地路径

Jasen Ye

abap 文件路径

地狱开局的2022,穿好你的安全铠甲

脑极体

Nginx限速模块初探

喀拉峻

nginx

什么是数据恢复?数据丢失的最常见原因有哪些?

Ethereal

自动化知识图谱表示:从三元组到子图

第四范式开发者社区

人工智能 自动化 知识图谱

产品升级|1-2月合刊:多款重磅产品来袭

百度大脑

前端需要掌握的设计模式_语言 & 开发_微医大前端技术_InfoQ精选文章