js主要设计模式

53 阅读9分钟

内容概览

image.png

设计模式介绍

什么是设计模式

设计模式是解决问题的一种思想,和语言无关。在面向对象软件设计的工程中,针对特定的问题简洁优
雅的一种解决方案。通俗一点的说,设计模式就是符合某种场景下某个问题的解决方案,通过设计模式
可以增加代码的可重用性,可扩展性,可维护性,最终使得我们的代码高内聚、低耦合。
总的来说设计模式是前人总结出来的优秀的写代码经验,如果参考设计模式来写代码,就能跟大佬一样写出优秀的代码;如果不参考,按照自己的思路也能实现功能,只不过代码可能会写的比较烂。

设计模式的五大设计原则

  • 单一职责:一个程序只需要做好一件事。如果功能过于复杂就拆分开,保证每个部分的独立

  • 开放封闭原则:对扩展开放,对修改封闭。增加需求时,扩展新代码,而不是修改源代码。这是软件设计的终极目标。

  • 里氏置换原则:子类能覆盖父类,父类能出现的地方子类也能出现。

  • 接口独立原则:保持接口的单一独立,避免出现“胖接口”。也就是说接口的功能要尽量单一,这点目前在TS中运用到。

  • 依赖倒置原则:面向接口编程,依赖于抽象而不依赖于具体。使用方只专注接口而不用关注具体类的实现。俗称“鸭子类型”

里氏替换和依赖倒置可以用下面的代码进行理解说明:

abstract class AbstractDrink { abstract getName(): string; } 
class CocaCola extends AbstractDrink { getName(): string { return '可乐'; } } 
class Sprite extends AbstractDrink { getName(): string { return '雪碧'; } } 
class Fanta extends AbstractDrink { getName(): string { return '芬达'; } }   
class Customer { drink(drink: AbstractDrink) { console.log('喝' + drink.getName()); } } 
let customer = new Customer(); 
let cocaCola = new CocaCola(); 
let sprite = new Sprite(); 
let fanta = new Fanta(); 
customer.drink(cocaCola); 
customer.drink(sprite); 
customer.drink(fanta);

js主要设计模式

1.工厂模式

在不暴露创建对象的具体逻辑,而是将逻辑进行封装,那么它就可以被称为工厂。工厂模式又叫做静态工厂模式,由一个工厂对象决定创建某一个类的实例

例子

  • 一个服装厂可以生产不同类型的衣服,我们通过一个工厂方法类来模拟产出

class DownJacket {
  production(){
    console.log('生产羽绒服')
  }
}
class Sweater{
  production(){
    console.log('生产毛衣')
  }
}
class TShirt{
  production(){
    console.log('生产t恤')
  }
}
// 工厂类
class clothingFactory {
  constructor(){
    this.downJacket = DownJacket
    this.underwear = Underwear
    this.t_shirt = TShirt
  }
  getFactory(clothingType){
    const _production = new this[clothingType]
    return _production.production()
  }
}
const clothing = new clothingFactory()
clothing.getFactory('t_shirt')// 生产t恤

优点

  1. 调用者创建对象时只要知道其名称即可
  2. 扩展性高,如果要新增一个产品,直接扩展一个工厂类即可。
  3. 隐藏产品的具体实现,只关心产品的类型。

缺点

  • 每次增加一个产品时,都需要增加一个具体类,这无形增加了系统内存的压力和系统的复杂度,也增加了具体类的依赖

2.抽象工厂模式

  • 抽象工厂模式就是通过类的抽象使得业务适用于一个产品类簇的创建,而不负责某一个类产品的实例。抽象工厂可以看作普通工厂的升级版,普通工厂以生产实例为主,而抽象工厂的目就是生产工厂。

例子

在后台管理系统中,具有三类权限的用户,分别是超级管理员、管理员和编辑人员。抽象工厂创建的是每一种用户的类,而不是具体类的实例。

class User {
        constructor(name, role, page) {
          this.name = name;
          this.role = role;
          this.page = page;
        }
        welcome() {
          console.log("欢迎回来", this.name);
        }
        dataShow() {
          throw new Error("抽象方法必须被实现");
        }
        static UserFactory(role) {
          switch (role) {
            case "superadmin":
              return new User("superadmin", [
                "home",
                "user-manage",
                "right-manage",
                "news-manage",
              ]);
              break;
            case "admin":
              return new User("admin", ["home", "user-manage", "news-manage"]);
              break;
            case "editor":
              return new User("editor", ["home", "news-manage"]);
              break;
            default:
              throw new Error("param error");
          }
        }
      }
      class SuperAdmin extends User {
        constructor(name) {
          super(name, "superadmin", [
            "home",
            "user-manage",
            "right-manage",
            "news-manage",
          ]);
        }

        dataShow() {
          console.log("superadmin-data-show");
        }

        addRight() {}
        addUser() {}
      }
      class Admin extends User {
        constructor(name) {
          super(name, "admin", ["home", "user-manage", "news-manage"]);
        }

        dataShow() {
          console.log("admin-data-show");
        }

        addUser() {}
      }
      class Editor extends User {
        constructor(name) {
          super(name, "editor", ["home", "news-manage"]);
        }

        dataShow() {
          console.log("editor-data-show");
        }
      }
      function getAbstractUserFactory(role) {
        switch (role) {
          case "superadmin":
            return SuperAdmin;
          case "admin":
            return Admin;
          case "editor":
            return Editor;
          default:
            throw new Error("param error");
        }
      }
      const UserClass = getAbstractUserFactory("editor");
      let user = new UserClass("kerwin");
      user.dataShow();
      //工厂模式返回一个类的具体实例,抽象工厂模式返回的是一个类

优点

  1. 当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象
  2. 扩展性高,如果要一类产品,直接扩展一个工厂类即可。

缺点

  • 每次增加一类产品时,都需要增加一个具体类,这无形增加了系统内存的压力和系统的复杂度

3.单例模式

  • 单例模式的思路是:保证一个类只能被实例一次,每次获取的时候,如果该类已经创建过实例则直接返回该实例,否则创建一个实例保存并返回。

  • 单例模式的核心就是创建一个唯一的对象,而在javascript中创建一个唯一的对象太简单了,为了获取一个对象而去创建一个类有点多此一举。如const obj = {}obj就是独一无二的一个对象,在全局作用域的声明下,可以在任何地方对它访问,这就满足了单例模式的条件,但这种方式有个问题容易导致全局污染,出现命名冲突等问题。所以这种方式在使用的时候需要特别注意。

例子

  • 我们常见到的登录弹窗,要么显示要么隐藏,不可能同时出现两个弹窗,下面我们通过一个类来模拟弹窗。
class LoginFrame {
    static instance = null
    constructor(state){
        this.state = state
    }
    show(){
        if(this.state === 'show'){
            console.log('登录框已显示')
            return
        }
        this.state = 'show'
        console.log('登录框展示成功')
    }
    hide(){
        if(this.state === 'hide'){
            console.log('登录框已隐藏')
            return
        }
        this.state = 'hide'
        console.log('登录框隐藏成功')
    }
    // 通过静态方法获取静态属性instance上是否存在实例,如果没有创建一个并返回,反之直接返回已有的实例
    static getInstance(state){
        if(!this.instance){
            this.instance = new LoginFrame(state)
        }
        return this.instance
    }
}
const p1 = LoginFrame.getInstance('show')
const p2 = LoginFrame.getInstance('hide')
console.log(p1 === p2) // true

优点

  1. 内存中只有一个实例,减少了内存的开销

缺点

  • 如果是全局变量的形式,容易导致全局污染和命名冲突
  • 如果是类的形式,违反了单一职责,一个类应该只关心内部逻辑,而不用去关心外部的实现(全局)

4.装饰器模式

  • 装饰者模式能够在不更改源代码自身的情况下,对其进行职责添加。相比于继承装饰器的做法更轻巧。通俗的讲我们给手机上贴膜,带手机壳,贴纸,这些就是对手机的装饰。如果某个方法需要该功能,就对其进行注入,不需要则不注入

例子

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script>
      Function.prototype.before = function(beforeFn) {
        const _this = this;
        return function() {
          //先进行前置函数调用
          beforeFn.apply(this, arguments);
          //执行原来的函数
          return _this.apply(this, arguments);
        };
      };
      Function.prototype.after = function(afterFn) {
        const _this = this;
        return function() {
          //先执行原来的函数
          const res = _this.apply(this, arguments);
          //后进行后置函数调用
          afterFn.apply(this, arguments);
          return res;
        };
      };
      function log() {
        console.log("上传uv pv数据");
      }
      function render(params) {
        console.log("页面处理逻辑");
      }
      render = render.before(log);
      //装饰器:将某个功能抽离出来,需要的时候注入(上报日志),不需要的时候则不注入
      //典型应用:axios的拦截器 需要用户登录的时候在请求拦截器里注入token,不需要登录的时候则不注入
      render();
      // filmbtn.onclick = function () {
      //   render()
      // }
    </script>
  </body>
</html>

优点

  1. 装饰类和被装饰类都只关心自身的核心业务,实现了解耦
  2. 方便动态的扩展功能,且提供了比继承更多的灵活性(装饰器更灵活,单个功能的实现,而继承需要把父类所有的方法都继承过来)

缺点

  • 多层的装饰会增加复杂度

5.适配器

适配器模式的目的是为了解决对象之间的接口不兼容的问题,通过适配器模式可以不更改源代码的情况下,让两个原本不兼容的对象在调用时正常工作。典型的案例就是220V的交流电没法直接给手机充电,需要充电器的适配。充电器就是一个适配器。

例子

axios源码在封装ajax和node请求方法的时候就使用了适配器模式,使得顶层在调用的时候接口暴露的一致

//获取默认的适配器
function getDefaultAdapter() {
    var adapter;
    if (typeof XMLHttpRequest !== 'undefined') {
        // For browsers use XHR adapter
        //引入用于发送 AJAX 请求的适配器
        adapter = require('./adapters/xhr');
    } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
        // For node use HTTP adapter
        //引入用于在 Node 端发送HTTP请求的适配器
        adapter = require('./adapters/http');
    }
    return adapter;
}

优点

  1. 让任何两个没有关联的类或方法可以同时有效运行,并且提高了复用性、透明度、以及灵活性

缺点

  • 非直接调用,适配的过程本身存在一定的开销

6.代理模式

代理模式的关键是,当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象。替身对象对请求做出一些处理之后,再把请求转交给本体对象

例子

最典型的应用就是vue得数据驱动模式,一旦数据改变了,通过代理拦截来进行dom操作或者页面的刷新

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="box"></div>
    <script>
      let vueobj = {};
      let proxy = new Proxy(vueobj, {
        get(target, key) {
          return target[key];
        },
        set(target, key, value) {
          if (key === "data") {
            //操作dom
            box.innerHTML = value;
          }
          target[key] = value;
        },
      });
      // 典型应用  vue的数据驱动模式 --> 数据一旦更改,页面进行刷新或者dom操作
      //proxy.data=111 访问代理才能被拦截,直接访问原有对象不行
    </script>
  </body>
</html>

优点

  1. 代理模式能将代理对象与被调用对象分离,降低了系统的耦合度。代理模式在客户端和目标对象之间起到一个中介作用,这样可以起到保护目标对象的作用

缺点

  • 非直接调用存在访问开销

7.观察者模式(发布订阅模式)

定义了一种一对多的关系,让多个观察者对象同时监听某一个主题对象,这个主题对象的状态发生变化时就会通知所有的观察者对象,使它们能够自动更新自己

例子

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script>
      class Subject {
        constructor() {
          this.observers = [];
        }

        add(observer) {
          this.observers.push(observer);
        }

        remove(observer) {
          this.observers = this.observers.filter((item) => item !== observer);
        }

        notify() {
          this.observers.forEach((item) => {
            console.log(item);
            item.update();
          });
        }
      }

      class Observer {
        constructor(name) {
          this.name = name;
        }
        update() {
          console.log("update", this.name);
        }
      }

      const subject = new Subject();
      const observer1 = new Observer("xiaoming");
      const observer2 = new Observer("xiaoli");

      subject.add(observer1);
      subject.add(observer2);
      //典型应用是redux统一管理数据的方式
    </script>
  </body>
</html>

优点

  1. 支持简单的广播通信,自动通知所有已经订阅过的对象
  2. 被观察者与观察者之间的抽象耦合关系能单独扩展以及重用

缺点

  • 当订阅者比较多的时候,同时通知所有的订阅者可能会造成性能问题
  • 在订阅者和订阅目标之间如果循环引用执行,会导致崩溃