简介

  1. 设计模式是一套被反复使用、多数人知晓、经过分类编目的、代码设计经验的总结。它是为了可重用代码,让代码更容易的被他人理解并保证代码的可靠性
  2. 设计模式的几大原则
    • 单一职责原则
    • 开放封闭原则
    • 里式替换原则
    • 接口隔离原则
    • 依赖反转原则
    • 最少知识原则
  3. 常见的12种设计模式
    • 工厂模式
    • 单例模式
    • 观察者模式
    • 发布-订阅模式
    • 原型模式
    • 适配器模式
    • 装饰者模式
    • 策略模式
    • 模块模式
    • 代理模式
    • 迭代器模式
    • 状态模式

工厂模式

  1. 工厂模式通过工厂方法来创建对象,而不是直接使用 new 关键字,工厂方法根据输入参数的不同,决定创建哪个具体的对象实例并将其返回。
  2. 工厂模式就是根据不用的输入返回不同的实例,一般用来创建同一类对象,它的主要思想就是将对象的创建与对象的实现分离,在创建对象时,不暴露具体的逻辑,而是将逻辑封装在函数中,那么这个函数就可以视为一个工厂,工厂模式根据抽象程度的不同可以分为: 简单工厂、工厂方法、抽象工厂
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 定义产品类
class Product {
constructor(name) {
this.name = name;
}

display() {
console.log(`product: ${this.name}`);
}
}
// 定义工厂类
class Factory {
createProduct(name) {
return new Product(name);
}
}

// 使用工厂创建对象

const factory = new Factory();

const p1 = factory.createProduct('p1');
const p2 = factory.createProduct('p2');

p1.display();
p2.display();
  1. 使用场景
    • 当需要创建多个相似的对象时
    • 当对象创建过程复杂或需要隐藏创建逻辑时
    • 当希望通过公共的接口来创建对象时
  2. 优点
    • 将对象的创建与使用代码分离,客户端只需关注接口而不需要关心具体的对象创建过程
    • 可以通过工厂方法来创建不同类型的对象,提供灵活性和可扩展性
  3. vue 中的工厂模式
    • VNode,和原生的 document.createElement 类型,vue 这种具有虚拟 dom 树(virtual dom tree) 机制的框架在生成虚拟 dom 的时候,提供了 createElement 方法来生成 VNode 用来作为真实 dom 节点的映射

      1
      2
      3
      4
      createElement('h3', { class: 'main-title' }, [
      createElement('img', { class: 'avatar', attrs: { src: '../avatar.jpg' }}),
      createElement('p', { class: 'user-desc' }, 'hello world')
      ])
      1
      2
      3
      4
      5
      6
      7
      8
      9
      // createElement 函数结构

      class Vnode(tag, data, children) {
      // ...
      }

      function createElement(tag, data, children) {
      return new Vnode(tag, data, children);
      }
    • vue-routevue 进行路由创建模式中,也使用了工厂模式

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      export default class VueRouter {
      constructor(options) {
      this.mode = mode; // 路由模式

      switch (mode) { // 简单工厂
      case 'history': //
      this.history = new HTML5History(this, options.base);
      break;
      case 'hash': // hash 方式
      this.history = new HashHistory(this, options.base, this.fallback);
      break;
      case 'abstract': // abstract 方式
      this.history = new AbstractHistory(this, options.base);
      break;
      default:
      // ...初始化失败报错
      }
      }
      }
      • mode 是路由创建的模式,这里有三种 History、Hash、Abstract,其中,HistoryH5 的路由方式,Hash 是路由中带 # 的路由方式,Abstract 代表非浏览器环境中路由方式,比如 Node、weex 等;this.history 用来保存路由实例,vue-router 中使用了工厂模式的思想来获得响应路由控制类的实例

单例模式

  1. 单例模式确保一个类只有一个实例,并提供全局访问点来获取该实例,它通过私有化构造函数限制外部直接创建对象,并提供一个静态方法来获取或创建唯一的实例
  2. 单例模式又叫单体模式,保证一个类只有一个实例,并提供一个访问它的全局访问点,也就是说,第二次使用同一个类创建新对象的时候,应该得到与第一次创建的对象完全相同的对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Singleton {
static instance = null;

constructor() {
if (Singleton.instance) {
return Singleton.instance;
}

Singleton.instance = this;
}
}

const ins1 = new Singleton();
const ins2 = new Singleton();

console.info(ins1 === ins2); // true
  1. 使用场景

    • 当只需要一个全局对象来协调系统中的操作时
    • 当需要频繁访问同一个对象实例时
    • 当需要限制一个类只有一个实例
  2. 优点

    • 提供了对唯一实例的全局访问,方便共享对象
    • 避免了重复创建实例的开销,节省了内存和资源
  3. vue 的单例模式

    • element-ui loading
      • 指令模式: Vue.use(Loading.directive),使用 `
      • 服务模式: Vue.prototype.$loading = service,使用 this.$loading({fullscreen: true})
      • 用服务方法使用全屏 loading 是单例的,即在前一个全屏 loading 关闭前再次调用全屏 loading 并不会创建一个新的 loading 实例,而是返回现有全屏 Loading 的实例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    /**
    这里的单例是 fullScreenLoading,是存放在闭包中的,如果用户传的 options 的 fullscreen 为 true 且已经创建了单例,则直接返回之前创建的单例,如果之前没有创建过,则创建单例并赋值给闭包中的 fullScreenLoading 后返回新创建的单例实例
    */
    import Vue from 'vue';
    import LoadingVue from './loading.vue';

    const LoadingContructor = Vue.extend(LoadingVue);

    let fullScreenLoading; // 单例

    const Loading = (options = {}) => {
    if (options.fullscreen && fullScreenLoading) {
    return fullScreenLoading;
    };

    let instance = new LoadingContructor({
    el: document.createElement('div'),
    data: options
    });

    if (options.fullscreen) {
    fullScreenLoading = instance;
    }

    return instance;
    }

    export default Loading;
    • Vuex,一个专为 Vue.js 应用程序开发的状态管理模式。Vuex,它们都实现了一个全局的 Store 用于存储应用的所有状态。这个 Store 的实现,正是单例模式的典型应用

      • Vuex 使用单一状态树,用一个对象就包含了全部的应用层级状态。至此它便作为一个“唯一数据源 (SSOT)”而存在。这也意味着,每个应用将仅仅包含一个 store 实例。单一状态树让我们能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照
      1
      2
      3
      4
      5
      6
      Vue.use(Vuex);

      new Vue({
      el: '#app',
      store
      })
      • 通过调用 Vue.use() 方法,安装了 Vuex 插件。Vuex 插件是一个对象,它在内部实现了一个 install 方法,这个方法会在插件安装时被调用,从而把 Store 注入到Vue 实例里去。也就是说每 install 一次,都会尝试给 Vue 实例注入一个 Store
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      let Vue; // 单例

      export function install(_Vue) {
      // 判断传入的Vue实例对象是否已经被install过Vuex插件(是否有了唯一的state)
      if(Vue && _Vue === Vue) {
      if (process.env.NODE_ENV !== 'production') {
      console.error('[vuex] already installed. Vue.use(Vuex) should be called only once.');
      }
      return;
      }

      Vue = _Vue; // 若没有,则为这个Vue实例对象install一个唯一的Vuex
      // 将Vuex的初始化逻辑写进 Vue 的钩子函数里
      // 可以保证一个 Vue 实例(即一个 Vue 应用)只会被 install 一次 Vuex 插件,所以每个 Vue 实例只会拥有一个全局的 Store
      applyMixin(Vue);
      }

观察者模式

  1. 观察者模式定义了对象之间的一对多依赖关系,当一个对象的状态发生变化时,它的所有依赖者(观察者)都会被通知和更新
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Subject {
constructor() {
this.observers = [];
}

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

removeObserver(observer) {
this.observers = this.observers.filter(obs => obs !== observer);
}

notifyObservers() {
this.observers.forEach(obs => obs.update());
}
}

class Observer {
constructor(name) {
this.name = name;
}

update() {
console.log(`Observer ${this.name} has been notified`);
}
}

// 创建主题和观察者
const subject = new Subject();

const obs1 = new Observer('1');
const obs2 = new Observer('2');

// 注册观察者
subject.addObserver(obs1);
subject.addObserver(obs2);

// 通知观察者
subject.addObserver();
  1. 使用场景
    • 当一个对象的变化需要通知其他对象,以便它们可以做出相应的响应时
    • 当对象之间的耦合度需要降低,使得它们可以独立地交互时
  2. 优点
    • 实现了对象之间的松耦合,被观察者和观察者可以独立地演化和变化
    • 可以轻松添加或移除观察者,以实现动态的发布-订阅机制

发布-订阅模式

  1. 发布-订阅模式类似于观察者模式,但是发布者(或称为主题)不会直接通知特定的订阅者,而是通过消息代理(或称为事件总线)来分发和传递消息。订阅者可以根据自身的需求订阅感兴趣的消息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class EventBus {
constructor() {
this.subscriber = {};
}

subscribe(eventName, callback) {
if (!this.subscribers[eventName]) {
this.subscribers[eventName] = [];
}

this.subscribers[eventName].push(callback);
}

unsubscribe(eventName, callback) {
if (this.subscribers[eventName]) {
this.subscribers[eventName] = this.subscribers[eventName].filter(cb => cb !== callback);
}
}

publish(eventName, data) {
if (this.subscribers[eventName]) {
this.subscribers[eventName].forEach(callback => callback(data));
}
}
}
// 创建事件总线
const eventBus = new EventBus();

// 订阅事件总线
const callback1 = data => console.log(`Subscriber 1 received: ${data}`);
const callback2 = data => console.log(`Subscriber 2 received: ${data}`);

eventBus.subscribe('event1', callback1);
eventBus.subscribe('event1', callback2);

// 发布事件
eventBus.publish('event1', 'hello, subscribers');
  1. 使用场景

    • 当一个对象的状态变化需要通知多个订阅者时
    • 当需要将发布和订阅者解耦,使它们可以独立地演化时
    • 当希望在系统中引入中介层以提供更灵活的消息传递机制时
  2. 优点

    • 解耦了发布者和订阅者,使它们可以独立地交互
    • 提供了更灵活的消息传递机制,可以实现更复杂的事件处理逻辑
  3. 观察者模式和发布-订阅模式的区别

    • 观察者模式是由具体目标调度的,而发布-订阅模式是统一由调度中心调的
    发布订阅模式和观察者模式的区别
    发布订阅模式和观察者模式的区别
  4. vue 中的发布-订阅模式

    • EventBusvue 中的一种事件机制,可以用来解决组件间数据通信
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 创建事件中心管理组件之间的通信
    import Vue from 'vue';

    export const EventBus = new Vue();

    // 组件中使用 xxx.vue
    // 发送事件
    EventBus.$emit('evnetName', {num: 1});
    // 接收事件
    EventBus.$on('eventName', ({ num }) => console.log(num));
    • vue 双向绑定机制
    vue响应式
    vue响应式
    • 响应式化大致是使用 Object.defineProperty 把数据转为 getter/setter,并为每个数据添加一个订阅者列表的过程,这个列表是 getter 闭包中的属性,将会记录所有依赖这个数据的组件,也就是说,响应化后的数据相当于发布者
    • 每个组件都对应一个 Watcher 订阅者,当每个组件的渲染函数被执行时,都会将本组件的 watcher 放到自己所依赖的响应式数据的订阅者列表里,这就相当于完成了订阅,一般这个过程被称为依赖收集 Dependency Collect
    • 组件渲染函数执行的结果是生成虚拟 dom (virtual dom tree),这个树生成后将被映射为浏览器上的真实的 dom 树,也就是用户所看到的页面列表
    • 当响应式数据发生变化时,也就是触发了 setter 时,setter 会负责通知 notify 该数据的订阅者列表里的 watcherwatcher 会触发组件重新渲染(trigger re-render) 来更新 update 视图
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // src/core/observers/index.js

    Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
    // ...
    const value = getter ? getter.call(obj) : val; // 如果原本对象拥有 getter 方法则执行
    dep.depend(); // 进行依赖收集 ,dep.addSub

    return value;
    },
    set: function reactiveSetter(newValue) {
    // ...
    if (setter) {
    setter.call(obj, newValue); // 如果原本对象拥有 setter 方法则执行
    }

    dep.notify()// 如果发生变更,则通知更新
    }
    })

原型模式

  1. 原型模式通过克隆现有对象来创建新对象,而不是依赖显式的实例化过程。每个对象都可以作为另一个对象的原型,新对象会继承原型对象的属性和方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Prototype {
constructor(name) {
this.name = name;
}

clone() {
return Object.create(this);
// Object.getPrototypeOf(object); 返回指定对象的原型(即内部 [[Prototype]] 属性的值)
// Object.create(object); 以该对象为原型创建一个对象
}
}
// 创建原型对象
const prototype = new Prototype('prototype');

// 克隆对象
const clone1 = prototype.clone();
const clone2 = prototype.clone();

console.log(clone1.name);
console.log(clone2.name);

prototype.name = 'console';

console.log(clone1.name);
console.log(clone2.name);
  1. 使用场景
    • 当创建对象的过程比较昂贵或复杂时,而且新对象的创建与现有对象的状态无关时
    • 当希望通过修改原型来影响所有克隆对象时
    • 当需要避免使用 new 关键字直接实例化对象时
  2. 优点
    • 避免了创建对象的昂贵或复杂过程,提高了性能和效率
    • 可以通过修改原型对象来影响所有克隆对象,实现了对象状态的批量修改

适配器模式

  1. 适配器模式将一个类的接口转换为另一个接口,以满足客户端的需求,它通过创建一个适配器类来实现接口转换,并在适配器类中调用被适配类的方法
  2. 适配器模式(Adapter Pattern) 又叫包装器模式,将一个类(对象)的接口(方法、属性)转化为用户需要的另一个接口,解决类(对象),之间接口不兼容的问题
    • 主要功能是进行转换匹配,目的是复用已有的功能,而不是来实现新的接口,也就是说,访问者需要的功能应该是已经实现好了的,不需要适配器模式来实现,适配器模式主要负责把不兼容的接口转换为访问者期望的格式而已
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Adaptee {
specificRequest() {
return 'Specific request';
}
}

class Adapter {
constructor(adaptee) {
this.adaptee = adaptee;
}

request() {
return this.adaptee.specificRequest();
}
}

// 使用适配器
const adaptee = new Adaptee();
const adapter = new Adapter(adaptee);
console.log(adapter.request()); //
  1. 使用场景

    • 当需要将一个已有类的接口转换为另一个接口时
    • 当希望通过一个统一的接口来使用多个不兼容的类时
    • 当需要在不影响现有代码的情况下,对已有类的方法进行扩展或修改时
  2. 优点

    • 可以将已有类与新代码进行无缝衔接,使它们可以协同工作
    • 可以实现对象之间的接口转换,提供了灵活性和可扩展性
  3. 适配器实际案例

    • vue 计算属性
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    export default {
    data() {
    return {
    message: 'hello'
    }
    },
    computed: {
    reversedMessage: function() {
    return this.message.split('').reverse().join('');
    }
    }
    }
    // 对原有数据并没有改变,只改变了原有数据的表现形式
    • axios 用来发送请求的 adapter 本质上是封装了浏览器提供的 XMLHttpRequest
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    module.exports = function xhrAdapter(config) {
    return new Promise(function dispatchXhrRequest(resolve, reject) {
    var requestData = config.data;
    var requestHeaders = config.headers;
    var request = new XMLHttpRequest();

    // 初始化请求
    request.open(config.method.toUpperCase(), buildURL(config.url, config.params, config.paramsSerializer), true);

    // 设置最大超时时间
    request.timeout = config.timeout;

    // readyState 属性发生变化时的回调
    request.onreadystatechange = function handleLoad() {
    // ...
    }

    // 浏览器请求退出时的回调
    request.onabort = function handleAbort() {
    // ...
    }

    // 当请求报错时的回调
    request.onerror = function handleError() {
    // ...
    }

    // 当请求超时调用的回调
    request.ontimeout = function handleTimeout() {
    // ...
    }

    // 设置请求头的值
    if ('setRequestHeader' in request) {
    request.setRequestHeader(key, val);
    }

    // 跨域请求是否应该使用证书
    if (config.withCredentials) {
    request.withCredentials = true;
    }

    // 响应类型
    if (config.responseType) {
    request.responseType = config.responseType;
    }

    // 发送请求
    request.send(requestData);
    })
    }

    /**
    *
    这个模块主要是对请求头、请求配置和一些回调的设置,并没有对原生的 API 有改动,所以也可以在其他地方正常使用。这个适配器可以看作是对 XMLHttpRequest 的适配,是用户对 Axios 调用层到原生 XMLHttpRequest 这个 API 之间的适配层
    */

装饰者模式

  1. 装饰者模式动态地给对象添加新的行为或功能,同时不改变其原始类结构,它通过创建一个装饰器类来包装原始对象,并在装饰器类中添加额外的行为
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Component {
operation() {
return 'Component operation';
}
}

class Decorator {
constructor(component) {
this.component = component;
}

operation() {
return `${this.component.operation()} + Decorator operation`;
}
}

// 使用装饰者
const com = new Component();
const decorator = new Decorator(com);
console.log(decorator.operation()); // 输出 component operation + Decorator operation
  1. 使用场景
    • 当需要在不改变现有对象结构的情况下,动态地给对象添加新的行为时
    • 当希望通过透明的方式为对象添加功能,而不影响其使用方式和客户端代码时
    • 当不适合使用子类来扩展对象功能时
  2. 优点
    • 可以透明地扩展对象的功能,而不影响客户端代码
    • 允许通过装饰器类组合和嵌套多个装饰器,实现复杂的功能组合

策略模式

  1. 策略模式定义了一系列算法,将它们封装为独立的可互换的策略对象,并使得客户端可以在运行时动态地选择不同的策略。客户端通过与策略对象进行交互来实现不同的行为。策略模式定义了一系列算法,将它们一个个封装起来,并且使它们可以互相替换。封装的策略算法一般是独立的,策略模式根据输入来调整采用哪个算法。关键是策略的实现和使用分离
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Strategy {
execute() {
// 策略执行的具体操作
}
}

class ConcreteStrategy1 extends Strategy {
execute() {
console.log('Strategy 1');
}
}

class ConcreteStrategy2 extends Strategy {
execute() {
console.log('Strategy 2');
}
}

// 使用策略
const st1 = new ConcreteStrategy1();
const st2 = new ConcreteStrategy2();

st1.execute();
st2.execute();
  1. 使用场景
    • 当需要在多个算法或行为之间进行动态选择时
    • 当希望将算法的实现与使用它的客户端代码分离,以便它们可以独立地演化和修改时
    • 当不希望使用大量的条件语句来处理不同的情况时
  2. 优点
    • 实现了算法的封装和多态性,可以根据需要灵活地切换算法
    • 将算法的实现与使用它的客户端代码分离,使得它们可以独立演化和修改
  3. 策略模式的实际使用
    • 表格 formatter

      • element ui 的表格控件的 column 接受收一个 formatter 参数来格式化内容,其类型为函数,并且还可以接收几个特定的参数,例如:Function(row, column, cellValue, index)
      • 以文件大小为例,后端经常会直接传入 bit 单位的文件大小,那么前端需要根据后端的数据,根据需求转化为需要的单位的文件大小,例如 kb/mb
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      // 首先实现文件计算的算法
      export default StrategyMap = {
      // StrategyMap 1. 将文件大小 bit 转为 kb
      bitToKB: val => {
      const num = Number(val);
      return isNaN(num) ? val : (num / 1024).toFixed(0) + 'KB'
      },
      // StrategyMap 2. 将文件大小 bit 转为 MB
      bitToMB: val => {
      const num = Number(val);
      return isNaN(num) ? val : (num / 1024 / 1024).toFixed(1) + 'MB';
      }
      }

      // Context: 生成el表单 formatter
      const strategyContext = function(type, rowKey) {
      return function(row, column, cellValue, index) {
      return StrategyMap[type](row[rowKey]);
      }
      }

      export default strategyContext;
    • 表单验证

      • 除了表格中的 formatter 之外,策略模式也经常用在表单验证的场景。element-uiform 表单具有表单验证功能,用来校验用户输入的表单内容。实际需求中表单验证项一般会比较复杂,所以需要给每个表单项增加 validator 自定义校验方法
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      // 姓名校验 由2-10位汉字组成 
      export function validateUsername(str) {
      const reg = /^[\u4e00-\u9fa5]{2,10}$/;
      return reg.test(str);
      }
      // 手机号校验 由以1开头的11位数字组成
      export function validateMobile(str) {
      const reg = /^1\d{10}$/;
      return reg.test(str);
      }

      export function validateEmail(str) {
      const reg = /^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/;
      return reg.test(str);
      }

      import * as Validates from '../validates.js';

      // 生成表单自定义校验函数
      export const formValidateGene = (key, msg) => (rule, value, cb) => {
      if (Validates[key](value)) {
      cb();
      } else {
      cb(new Error(msg));
      }
      }

模块模式

  1. 模块模式使用函数作用域和闭包来封装和组织代码,实现模块化和私有性。它通过返回一个包含公共方法和属性的对象,来实现对外部的封装
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const module = (function() {
let privateVariable = 'private';

function privateMethod() {
console.log('private method');
}

return {
publicVariable: 'public',
publicMethod: function() {
console.log('public method');
}
}
})();

console.log(module.publicVariable);
module.publicMethod();
  1. 使用场景
    • 当希望将相关的方法和属性封装在一个单独的对象时
    • 当希望限制对方法和属性的访问,并保持私有性时
    • 当需要实现模块化,避免全局命名冲突和污染时
  2. 优点
    • 将相关的方法和属性封装在一个单独的对象中,提供了组织和管理代码的方式
    • 通过闭包实现了私有性,可以隐藏内部实现细节,防止外部访问和修改

代理模式

  1. 代理模式为一个对象提供一个代理或占位符,并控制对其的访问,代理对象可以在访问被代理对象之前或之后添加额外的逻辑,如延迟加载、权限控制、缓存等。代理模式又称委托模式,它为目标对象创建一个代理对象,以控制对目标对象的访问。代理模式把代理对象插入到访问者和目标对象之间,从而为访问者对目标对象的访问引入一定的间接性。正是这种间接性,给了代理对象很多操作空间,比如在调用目标对象前和调用后进行一些预操作和后操作,从而实现新的功能或者扩展目标的功能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class RealSubject {
request() {
console.log('Real subject request');
}
}

class Proxy {
constructor(realSubject) {
this.realSubject = realSubject;
}

request() {
// 在调用真实对象之前或之后执行额外的操作
console.log('proxy request');
this.realSubject.request();
}
}

// 使用代理
const realSubject = new RealSubject();
const proxy = new Proxy(realSubject);

proxy.request();
  1. 使用场景
    • 当需要在访问对象之前或之后执行额外的操作
    • 当希望通过代理控制对象的访问权限时
    • 当需要延迟加载对象或实现缓存功能时
  2. 优点
    • 可以在访问对象之前或之后执行额外操作,如延迟加载、权限控制、缓存等
    • 提供了对真实对象的访问控制,可以限制对对象的直接访问
  3. 代理模式实际使用
    • 拦截器
      • axios 实例来进行 http 请求,使用拦截器 interceptor 可以提前对 request 请求和 response 响应返回进行一些预处理,比如
        • request 响应头、cookie 设置
        • 权限信息的预处理,常见的比如验权操作或 token 验证
        • 数据格式的格式化,比如对组件绑定的 date 类型的数据在请求前进行一些格式约定好的序列化操作
        • 空字符串的格式预处理,根据后端进行一些过滤操作
        • response 的一些通用报错处理,比如使用 message 控件抛出错误
        • 除了 http 相关的拦截器之外,还有路由跳转的拦截器,可以进行一些路由跳转的预处理等操作
    • 前端框架的数据响应化
      • Vue 2.x 中通过 Object.defineProperty 来劫持各个属性的 setter/getter,在数据变动时,通过发布-订阅模式发布消息给订阅者,触发相应的监听回调,从而实现数据的响应式化,也就是数据到视图的双向绑定
      • 为什么 Vue 2.x3.x 要从 Object.defineProperty 改用 Proxy 呢,是因为前者的一些局限性,导致的以下缺陷
        • 无法监听利用索引直接设置数组的一个项,例如 vm.items[indexOfItem] = newValue
        • 无法监听数组的长度的修改,例如 vm.items.length = newLength
        • 无法监听 es6Set weakSet Map WeakMap 的变化
        • 无法监听 Class 类型的数据
        • 无法监听对象属性的新加或删除

迭代器模式

  1. 迭代器模式提供了一种访问集合对象元素的方式,而无需暴露集合的内部结构,它将迭代逻辑封装在迭代器对象中,客户端通过迭代器来遍历集合
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 定义集合对象
class Collection {
constructor() {
this.items = [];
}

addItem(item) {
this.items.push(item);
}

getIterator() {
return new Iterator(this.items);
}
}

// 定义迭代器对象
class Iterator {
constructor(collection) {
this.collection = collection;
this.index = 0;
}

hasNext() {
return this.index < this.collection.length;
}

next() {
return this.collection[this.index++];
}
}

// 使用迭代器遍历集合
const collection = new Collection();
collection.addItem("item 1");
collection.addItem("item 2");
collection.addItem("item 3");

const iterator = collection.getIterator();

while (iterator.hasNext()) {
console.log(iterator.next());
}
  1. 使用场景
    • 当集合对象的内部结构可能经常变化时,使用迭代器可以减少对客户端代码的影响
    • 当需要对集合对象进行不同类型的遍历时,迭代器提供了统一的接口,使得遍历逻辑更加灵活和可扩展
    • 当需要在遍历过程中对集合元素进行筛选、过滤或转换时,可以通过迭代器来实现
  2. 优点
    • 将遍历集合的责任从客户端代码抽离出来,简化了客户端代码
    • 隐藏了集合的内部结构,提供了更好的封装性和安全性
    • 支持不同类型的集合,提供了统一的迭代接口

状态模式

  1. 状态模式运行对象在内部状态发生改变时改变其行为,看起来像是对象类发生了改变,它将每个状态封装在一个独立的类中,并允许对象在不同状态之间切换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 定义状态接口
class State {
handle(context) {
// 默认实现
}
}
// 定义具体状态类
class ConcreteStateA extends State {
handle(context) {
console.log('state A');
context.setState(new ConcreteSateB());
}
}
//
class ConcreteStateB extends State {
handle(context) {
console.log('sate B');
context.setState(new ConcreteStateA());
}
}

// 定义上下文
class Context {
constructor() {
this.state = new ConcreteStateA();
}

setState(state) {
this.state = state;
}

request() {
this.state.handle(this);
}
}

// 使用状态模式
const context = new Context();
context.request(); // 输出 stateA
context.request(); // B
context.request(); //A
  1. 使用场景
    • 当一个对象的行为取决于其内部状态,并且在不同状态下具有不同行为时,可以使用状态模式来管理状态转换和行为
    • 当需要在运行时根据条件动态地改变对象的行为时,状态模式提供了一种优雅的方式来实现
    • 当对象有大量的条件语句,而且随着状态的增加会变得更加复杂时,可以使用状态模式来简化代码结构
  2. 优点
    • 将对象的状态和行为封装在独立的类中,提高了代码的可读性和可维护性
    • 避免了使用大量的条件语句来处理不同的状态,简化了代码结构
    • 新增或修改状态变得更加容易,不会对其他状态产生影响