JS 设计模式速成指南(上)

大家好,这里是大家的林语冰。欢迎关注"前端俱乐部"~

地球人都知道,JS 是一门混合范型语言,所以JS 也支持面向对象编程,其中设计模式则号称后 ES6 时代 JS 或 TS 面向对象编程的最佳实践。

"设计模式 "源于 GOF(四人帮)合著出版的一部经典作品《设计模式:可复用的面向对象软件元素》,该书第一次完整科普了软件开发中设计模式的概念。

设计模式一般分为三大类:

  • 创建型模式
  • 结构型模式
  • 行为型模式

本期共享的是,JS 中面向对象编程和设计模式的集大成者:创建型模式和结构型模式

免责声明

本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考,英文原味版请传送 Quick Reference Guide to Design Patterns in JS

创建型模式

创建型模式(Creational)是一种设计模式类别,用于解决与对象创建情况相关的常见问题。

单例模式

单例模式(Singleton)限制特定对象的实例数量有且仅有 1 个。单例模式减少了全局变量的需求,规避了命名冲突的风险。

举个栗子,我们检查 constructor() 中是否已存在一个异性伴侣实例,或者是否需要新建一个新实例。

js 复制代码
// 异性伴侣
class Soulmate {
  constructor() {
    if (typeof Soulmate.instance === 'object') {
      return Soulmate.instance
    }

    Soulmate.instance = this

    return this
  }
}

export default Soulmate

当我们只需要一个类的唯一实例时,请使用单例模式。

原型模式

原型模式(prototype)基于具有默认属性值的现有对象创建新对象。

举个栗子,我们可以使用 clone 克隆方法,创建一个与其父对象相同品种的猫猫新实例。

js 复制代码
class Cat {
  constructor(color, type) {
    this.color = color
    this.type = type
  }

  clone() {
    return new Cat(this.color, this.type)
  }
}

export default Cat

当我们能且仅能在运行时实例化类时,请使用原型模式。

工厂模式

工厂模式(Factory)创建新对象,该对象委托给子类决定需要实例化的类。

举个栗子,MovieFactory 电影工厂决定创建哪种类型的电影。

js 复制代码
// 电影工厂
class MovieFactory {
  create(type) {
    if (type === '岛国动作片') return new Movie(type, 9)
    if (type === '科幻片') return new Movie(type, 99)
  }
}

class Movie {
  constructor(type, price) {
    this.type = type
    this.price = price
  }
}

export default MovieFactory

当我们希望由子类决定创建的对象时,请使用工厂模式。

抽象工厂

抽象工厂模式(abstract factory)按主题创建的新对象,而不指定其具体的类。

js 复制代码
// 前端框架工厂
function useFramework(lib) {
  if (lib === 'Vue') return useVue
  if (lib === 'React') return useReact
  return use$
}

function useVue() {
  return new Vue()
}

function useReact() {
  return new React()
}

function use$() {
  return new $()
}

class Vue {
  init() {
    return 'Vue'
  }
}

class React {
  init() {
    return 'React'
  }
}

class $ {
  init() {
    return 'jQuery'
  }
}

export default useFramework

当系统应该独立于其生成的内容的结构或表示方式时,请使用抽象工厂模式。

结构型模式

结构型模式是一种设计模式类别,用于解决对象和类场景的组合相关的常见问题。

适配器模式

适配器模式(adapter)允许类"梦幻联动",将一个类接口创建为另一个类接口。

举个栗子,我们使用了一个 AxiosAdapter 适配器,可以在当前的系统中使用遗留的 request() 方法,且可以支持新版的 NodeFetch

js 复制代码
class Fetch {
  constructor(url) {
    this.url = url
  }

  request() {
    return `https://${this.url}`
  }
}

class NodeFetch {
  constructor(url) {
    this.url = url
  }

  nodeRequest() {
    return `https://${this.url}.com`
  }
}

class AxiosAdapter {
  constructor(nodeFetch) {
    this.nodeFetch = nodeFetch
  }

  request() {
    return this.nodeFetch.nodeRequest()
  }
}

export { Fetch, NodeFetch, AxiosAdapter }

当我们需要使用现有类,但它们之间的接口不匹配时,请使用适配器模式。

桥接模式

桥接模式(bridge)允许我们的类中的一个接口根据我们接收的实例,以及我们需要返回的实例,构建不同的实现。

举个栗子,我们在 Soldier 士兵类型和 Weapon 武器类型之间建桥,这样我们就可以将 weapon 武器实例精准传递给 soldier 士兵实例。

js 复制代码
class Soldier {
  constructor(weapon) {
    this.weapon = weapon
  }
}

class SuperSoldier extends Soldier {
  constructor(weapon) {
    super(weapon)
  }

  attack() {
    return `超级士兵,武器:${this.weapon.get()}`
  }
}

class IronMan extends Soldier {
  constructor(weapon) {
    super(weapon)
  }

  attack() {
    return `钢铁侠,武器:${this.weapon.get()}`
  }
}

class Weapon {
  constructor(type) {
    this.type = type
  }

  get() {
    return this.type
  }
}

class Shield extends Weapon {
  constructor() {
    super('防护罩')
  }
}

class Rocket extends Weapon {
  constructor() {
    super('火箭')
  }
}

export { SuperSoldier, IronMan, Shield, Rocket }

当我们需要在运行时从抽象中使用特定实现时,请使用桥接模式。

组合模式

组合模式(composite)允许创建属性具有原始类型元素或对象集合的对象。集合中每个元素本身可以包含其他集合,从而创建深度嵌套的结构。

举个栗子,我们正在创建一个计算设备子系统,该系统存储在一个 Cabinet 主机箱中,每个元素可以是不同的实例。

js 复制代码
// 设备
class Equipment {
  getPrice() {
    return this.price || 0
  }

  getName() {
    return this.name
  }

  setName(name) {
    this.name = name
  }
}

class Pattern extends Equipment {
  constructor() {
    super()
    this.equipments = []
  }

  add(equipment) {
    this.equipments.push(equipment)
  }

  getPrice() {
    return this.equipments
      .map(equipment => {
        return equipment.getPrice()
      })
      .reduce((a, b) => {
        return a + b
      })
  }
}

class Cabinet extends Pattern {
  constructor() {
    super()
    this.setName('主机箱')
  }
}

// 各种设备
class FloppyDisk extends Equipment {
  constructor() {
    super()
    this.setName('软盘')
    this.price = 70
  }
}

class HardDrive extends Equipment {
  constructor() {
    super()
    this.setName('硬件驱动')
    this.price = 250
  }
}

class Memory extends Equipment {
  constructor() {
    super()
    this.setName('内存')
    this.price = 280
  }
}

export { Cabinet, FloppyDisk, HardDrive, Memory }

当我们想要表示对象的层次结构时,请使用组合模式。

装饰器模式

装饰器模式(decorator)允许在运行时动态扩展对象的行为。

举个栗子,我们使用装饰器来扩展应用的通知行为。

js 复制代码
class Notification {
  constructor(kind) {
    this.kind = kind || 'App'
  }

  getInfo() {
    return `${this.kind} 发来通知`
  }
}

// 公众号通知
class WeChatNotification extends Notification {
  constructor() {
    super('公众号')
  }

  setNotification(msg) {
    this.message = msg
  }

  getInfo() {
    return `${super.getInfo()},具体消息:${this.message}`
  }
}

// B 站通知
class BiliBiliNotification extends Notification {
  constructor() {
    super('bilibili')
  }

  getInfo() {
    return super.getInfo()
  }
}

export { WeChatNotification, BiliBiliNotification }

当我们想要在运行时扩展对象,而不影响其他对象时,请使用装饰器模式。

外观模式/门面模式

外观模式(facade)为子系统中的一组接口提供了一个简化接口。外观模式定义了更高阶的接口,使子系统更易用。

举个栗子,我们创建一个简单的 Cart 购物车接口,它将若干子系统(比如 Discount/Shipping/Fees 等)的所有复杂性抽象出来。

js 复制代码
// 购物车
class Cart {
  constructor() {
    this.discount = new Discount()
    this.shipping = new Shipping()
    this.fees = new Fees()
  }

  calc(price) {
    price = this.discount.calc(price)
    price = this.fees.calc(price)
    price += this.shipping.calc()

    return price
  }
}

// 折扣
class Discount {
  calc(value) {
    return value * 0.85
  }
}

// 物流
class Shipping {
  calc() {
    return 500
  }
}

// 费用
class Fees {
  calc(value) {
    return value * 1.1
  }
}

export default Cart

当我们想为复杂子系统提供简单接口时,请使用外观模式。

享元模式

享元模式(flyweight)通过有效共享一大坨颗粒化对象来节省内存。共享的享元对象是不可变的,换而言之,它们无法修改,因为它们表示与其他对象共享的特征。

举个栗子,我们正在管理和创建某种食谱或烹饪应用程序的原料。

js 复制代码
class Ingredient {
  constructor(name) {
    this.name = name
  }

  getInfo() {
    return `我是${this.name}原材料`
  }
}

class Ingredients {
  constructor() {
    this.ingredients = {}
  }

  create(name) {
    let ingredient = this.ingredients[name]
    if (ingredient) return ingredient

    this.ingredients[name] = new Ingredient(name)

    return this.ingredients[name]
  }
}

export { Ingredients }

当应用程序使用一大坨迷你对象,且其存储成本昂贵,或者其本体不重要时,请使用享元模式。

代理模式

代理模式(proxy)为另一个对象提供一个代理或占位符对象,并控制该对象的读写。

举个栗子,我们使用代理模式,来限制飞行员的年龄。

js 复制代码
class Plane {
  fly() {
    return '乌鸦做飞机'
  }
}

class PilotProxy {
  constructor(pilot) {
    this.pilot = pilot
  }

  fly() {
    return this.pilot.age < 18 ? `未成年` : new Plane().fly()
  }
}

class Pilot {
  constructor(age) {
    this.age = age
  }
}

export { Plane, PilotProxy, Pilot }

当对象受到严重约束,且无法履行其职责时,请使用代理模式。

高潮总结

本文主要科普了创建型模式和结构型模式,行为型模式则会在下篇完整补充,欢迎关注"前端俱乐部",随时获取最新前端前沿资讯。

本文轻科普了两大类设计模式,包括但不限于:

  • 创建型模式
    • 单例模式
    • 原型模式
    • 工厂模式
    • 抽象工厂模式
  • 结构型模式
    • 适配器模式
    • 桥接模式
    • 组合模式
    • 装饰器模式
    • 外观模式
    • 享元模式
    • 代理模式

本期话题是 ------ 你在实际开发中最常使用的是哪种设计模式?欢迎在本文下方自由言论,文明共享。

长期关注"前端俱乐部",坚持阅读,自律打卡,每天一次,进步一点。谢谢大家的点赞,掰掰~

相关推荐
周努力.几秒前
设计模式之单例模式
单例模式·设计模式
大丈夫在世当日食一鲲1 分钟前
Java中用到的设计模式
java·开发语言·设计模式
cypking16 分钟前
vue实现一个pdf在线预览,pdf选择文本并提取复制文字触发弹窗效果
前端·vue.js·pdf
飘逸飘逸19 分钟前
若依前后端分离版使用Electron打包前端Vue为Exe文件
前端·vue.js·electron·vue·ruoyi
入门级前端开发20 分钟前
npm install 报错ERESOLVE
前端·npm·node.js
anyup1 小时前
最终!我还是抛弃了 VSCode 这个开发工具
前端·aigc·visual studio code
木亦Sam1 小时前
前端安全之 CSRF 攻击的防御策略
前端
光影少年1 小时前
es6+新增特性有哪些
前端·javascript·es6
木亦Sam1 小时前
前端代码优化之函数节流与防抖技巧
前端
怪力乌龟2 小时前
go语言设计模式-适配器模式
设计模式·golang·适配器模式