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 小时前
【每日学点鸿蒙知识】AVCodec、SmartPerf工具、web组件加载、监听键盘的显示隐藏、Asset Store Kit
前端·华为·harmonyos
alikami1 小时前
【若依】用 post 请求传 json 格式的数据下载文件
前端·javascript·json
wakangda1 小时前
React Native 集成原生Android功能
javascript·react native·react.js
吃杠碰小鸡2 小时前
lodash常用函数
前端·javascript
emoji1111112 小时前
前端对页面数据进行缓存
开发语言·前端·javascript
泰伦闲鱼2 小时前
nestjs:GET REQUEST 缓存问题
服务器·前端·缓存·node.js·nestjs
m0_748250032 小时前
Web 第一次作业 初探html 使用VSCode工具开发
前端·html
一个处女座的程序猿O(∩_∩)O2 小时前
vue3 如何使用 mounted
前端·javascript·vue.js
m0_748235952 小时前
web复习(三)
前端
User_undefined2 小时前
uniapp Native.js原生arr插件服务发送广播到uniapp页面中
android·javascript·uni-app