JavaScript 中常见的设计模式

339 阅读3分钟

前言

今天我们分享下四种常见的设计模式:观察者模式、发布订阅者模式、单例模式、工厂模式,设计模式学习的是一种思想,没有最好,只有更好

观察者模式

栗子:c 是某公司的领导,a 和 b 都加了 c 的联系方式,一天 c 发布了一个职位并且公布了职位的年龄标准,a 和 b 同时都能接收到,并且知道了自己是否通过

class FrontendPost {
  subs = [];
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  subscribe(target, sub) {
    target.subs.push(sub);
  }
  publish(value) {
    for (const sub of this.subs) {
      sub(value);
    }
  }
}

const a = new FrontendPost('a', 18);
const b = new FrontendPost('b', 28);
const c = new FrontendPost('c', 20);

// 需要去订阅 c
a.subscribe(c, (value) => {
  if (a.age <= value) {
    console.log('我是 a,I passed');
  } else {
    console.log('我是 a,I failed');
  }
});

// 需要去订阅 c
b.subscribe(c, (value) => {
  if (b.age <= value) {
    console.log('我是 b,I passed');
  } else {
    console.log('我是 b,I failed');
  }
});

// 最后由 c 来发布自己的事件
c.publish(c.age);

应用场景

  • vue2 的响应式原理应用的就是观察者模式(讲解一下响应式的步骤,涉及观察者模式的地方代码讲解一下)
  1. 存在 Vue 类,首先会去 new Vue 创建实例,Vue 类里面:

    • 使用 Observer 类劫持了 data 中的数据,也就是使用 Object.defineProperty (vue3 使用 proxy),拦截数据中的 get/set
    • 使用 Compiler 类解析模板中的 vue 指令,将数据渲染到模板中,最后挂载到节点上,Compiler 类中使用了 CompilerUtil 类,在其中进行 Watcher 的实例化
  2. 下面讲一下 Watcher 类:

    • Watcher 是 Observer 和 Compiler 的桥梁,帮助 Observer 收集依赖,触发 Compiler 更新函数
  3. 看下 Observer 、Watcher、 Dep(观察者模式中的收集依赖的作用) 类中的代码

    • Observer 数据劫持
    class Observer {
      constructor(data) {
        this.observer(data);
      }
      observer(obj) {
        if (obj && typeof obj === 'object') {
          for (let key in obj) {
            this.defineRecative(obj, key, obj[key]);
          }
        }
      }
      defineRecative(obj, attr, value) {
        this.observer(value);
        // 创建了属于当前属性的依赖收集实例
        let dep = new Dep();
        Object.defineProperty(obj, attr, {
          get() {
            // 在这里收集依赖
            Dep.target && dep.addSub(Dep.target);
            return value;
          },
          set: (newValue) => {
            if (value !== newValue) {
              this.observer(newValue);
              value = newValue;
              // 通知更新
              dep.notify();
            }
          },
        });
      }
    }
    
    • Dep 类:收集依赖,观察者模式中的 subs 数组和这边的 subs 一个作用
    class Dep {
      constructor() {
        this.subs = [];
      }
      addSub(watcher) {
        this.subs.push(watcher);
      }
      notify() {
        this.subs.forEach((watcher) => watcher.update());
      }
    }
    
    • Watcher 类:这边做了 Dep.target = this 指向自己的实例,收集好实例,然后再将实例释放
    class Watcher {
      constructor(vm, attr, cb) {
        this.vm = vm;
        this.attr = attr;
        this.cb = cb;
        this.oldValue = this.getOldValue();
      }
      getOldValue() {
        Dep.target = this;
        let oldValue = CompilerUtil.getValue(this.vm, this.attr);
        Dep.target = null;
        return oldValue;
      }
      update() {
        let newValue = CompilerUtil.getValue(this.vm, this.attr);
        if (this.oldValue !== newValue) {
          this.cb(newValue, this.oldValue);
        }
      }
    }
    
  4. 关键代码列出来了,分析一波:

    • 首先 Observer 劫持所有 data 中的数据,
    • 对于每个属性,会使用 Dep 创建属于每个属性的依赖收集实例,
    • 在 get(劫持) 方法中将 Watcher 实例推进 subs 数组,
    • 在 set(劫持) 方法中调用 Dep 创建实例的 notify 方法,也就是遍历 subs 数组中的实例,并且调用实例的 update 方法,更新视图
    • 显而易见,我们创建了对象上某个属性的实例,并且将依赖当前属性的所有 Watcher 实例(所有使用相同指令的不同节点)推进 subs 数组,所以是观察者模式

发布订阅者模式

栗子:a 和 b 都关注了某个平台的某个职位并且开启了提醒,当 c 上线该平台公布了该岗位的年龄要求之后,a 和 b 都会收到通知,并且知道自己是否通过

//调度中心
class Topic {
  static subs = {};
  static subscribe(key, sub) {
    if (!this.subs[key]) {
      this.subs[key] = [];
    }
    this.subs[key].push(sub);
  }
  static publish(key, value) {
    if (!this.subs[key]) return;
    for (const sub of this.subs[key]) {
      sub(value);
    }
  }
}

class FrontendPost {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  subscribe(key, sub) {
    Topic.subscribe(key, sub);
  }
  publish(key, value) {
    Topic.publish(key, value);
  }
}

const a = new FrontendPost('a', 18);
const b = new FrontendPost('b', 28);
const c = new FrontendPost('c', 38);

// 向调度中心注册 post 事件
a.subscribe('post', (value) => {
  if (a.age <= value) {
    console.log('我是 a,I passed');
  } else {
    console.log('我是 a,I failed');
  }
});

// 向调度中心注册 post 事件
b.subscribe('post', (value) => {
  if (b.age <= value) {
    console.log('我是 b,I passed');
  } else {
    console.log('我是 b,I failed');
  }
});

c.publish('post', c.age);

观察者模式和发布订阅者模式的差别

  • 发布订阅者模式和观察者模式最大的差别在于:发布订阅者模式有一个事件调度中心
  • 发布订阅模式的耦合比较松,观察者模式的耦合比较紧

new5.png

单例模式

  • 限制类只能有一个实例
  • 提供为全局可访问
  • 减少不必要的开销
  1. 全局变量
let _instance = null
function getSingle(){
  if (!_instance){
    _instance = this
  }
  return _instance
}
​
let m1 = new getSingle()
let m2 = new getSingle()
​
console.log(m1 === m2) //true
  1. 使用闭包
const getSingle = (function () {
  let _instance = null;
  function getInstance() {}
  return function () {
    if (!_instance) {
      _instance = new getInstance();
    }
    return _instance;
  };
})();
let a = getSingle();
let b = getSingle();
console.log(a === b);
  1. 类的静态属性
class GetSingle {
  static instance = '21';
  static getInstance() {
    if (!this.instance) {
      this.instance = new GetSingle();
    }
    return this.instance;
  }
}
let a = GetSingle.getInstance();
let b = GetSingle.getInstance();
console.log(a === b);

应用场景

  • 引入第三方库(多次引用,只会使用一次库的引用)
  • Vue 框架中生使用的 new Vue 就应用了单例模式
  • 一个全局使用的类频繁地被创建和销毁,占用内存,比如 Modal、Loading

工厂模式(简单工厂模式)

  1. 创建对象
  • 工厂模式就是创建对象的一种方式
  • 创建对象,降低代码冗余度
  • 当你想要批量生产同类型的对象的时候
function Idiot(name, age) {
  this.name = name;
  this.age = age;
}

const a = new Idiot('a', 18);
const b = new Idiot('b', 28);
console.log(a, b);
  1. 不同类的实例化
class Football {
  name = 'football';
  isChineseTeam() {
    console.log('of course not');
  }
}
class Basketball {
  name = 'basketball';
  constructor(name) {
    this.name = name;
  }
  isChineseTeam() {
    console.log('there may be');
  }
}

// ball 工厂
const BallFactory = function (name) {
  switch (name) {
    case 'NBA':
      return new Basketball();
    case 'worldCup':
      return new Football();
  }
};

const football = new BallFactory('worldCup');
console.log(football);
football.isChineseTeam();

安全模式类

  • 出现以下情况,忘记加 new 关键字
function Idiot(name, age) {
  this.name = name;
  this.age = age;
}
Idiot.prototype.say = () => {
  console.log('i am idiot');
};

const a = new Idiot('a', 18);
const b = Idiot('b', 28);
a.say();
b.say();
  • 改造,做一个兼容,加上 new 关键字
function Idiot(name, age) {
  if (!(this instanceof Idiot)) {
    return new Idiot(name, age);
  }
  this.name = name;
  this.age = age;
}
Idiot.prototype.say = () => {
  console.log('i am idiot');
};

const a = new Idiot('a', 18);
const b = Idiot('b', 28);
a.say();
b.say();

抽象工厂模式

  • 抽象类是一种声明式的类,使用时候报错,定义子类应该具备的方法,如果子类没有需要报错,只用于继承
  • 定义类中必备的方法,子类未重写则报错
class Ball {
  constructor() {
    if (new.target === Ball) {
      throw new Error('抽象类不能被实例化');
    }
  }
  isChineseTeam() {
    throw new Error('需要重写');
  }
}
class Football extends Ball {
  name = 'football';
  isChineseTeam() {
    console.log('of course not');
  }
}
class Basketball extends Ball {
  name = 'basketball';
  constructor(name) {
    this.name = name;
  }
  isChineseTeam() {
    console.log('there may be');
  }
}

// ball 工厂
const BallFactory = function (name) {
  switch (name) {
    case 'NBA':
      return new Basketball();
    case 'worldCup':
      return new Football();
  }
};

const football = new BallFactory('worldCup');
console.log(football);
football.isChineseTeam();

后续

当我们去看某些框架的源码的时候,不应当仅仅为了面试,我们应当去看到它们的优秀可取之处,拓展自己的思维,应该学习到优秀的框架设计,而不是仅仅又会了一道面试题而已,共勉。