JS设计模式笔记

25 阅读5分钟

一、什么是设计模式

  • 设计模式是我们在开发过程中针对特定问题而给出的更简洁而优化的处理方案
  • 在 JS 设计模式中,最核心的思想就是:封装
  • 设计的目的是让变与不变分离,让需要变化的更灵活,让不变的更稳定

二、构造器模式

  • 如果我们需要定义多个结构相似的对象,一个一个写很繁琐,如下:
let person1 = {
    name: '张三',
    age: '26'
}
let person2 = {
    name: '李四',
    age: '23'
}
  • 我们可以搭建一个构造函数,使我们的定义更快捷,省去重复的工作:
function Person (name, age){
    this.name = name;
    this.age = age;
    this.say = (msg) => {
        console.log(`${this.name}说${msg}`);
    }
}

let person1 = new Person('张三', '26');
person1.say('吃了么您?');
let person2 = new Person('李四', '23');
person2.say('刚吃完。');

三、原型模式

  • 上面我们每次生成一个对象时,里面都会生成一个一样的 say 函数并占用内存,为了更节省我们可以把这种重复的东西存在构造函数的原型上:
function Person (name, age){
    this.name = name;
    this.age = age;
}

// 注意:这里不能用箭头函数,不然会丢失this指向
Person.prototype.say = function(msg) {
    console.log(`${this.name}${msg}`);
}

let person1 = new Person('张三', '26');
person1.say('吃了么您?');
let person2 = new Person('李四', '23');
person2.say('刚吃完。');
  • 改为 es6 class类 写法
class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    // 方法挂在这里就等同于:Person.prototype.say = function(){....}
    say(msg) {
        console.log(`${this.name}${msg}`);
    }
}

let person1 = new Person('张三', '26');
person1.say('吃了么您?');
let person2 = new Person('李四', '23');
person2.say('刚吃完。');
  • 这样共用的方法就只存了一份在原型上,不用每次都生成扎讷你存了;其实Object和Array的那些原生方法都是挂在他们构造函数的原型上的。

三、工厂模式

1. 简单工厂模式

简单工厂模式就是你给工厂什么,工厂就给你生产什么,没有的话就生产不了;它适用于对象数量较少且创建的逻辑固定不复杂的场景:

// 我们搭建一个工厂生产不同产品

// 生产手机
class PhoneCreat {
    constructor(brand, colors) {
        this.brand = brand;
        this.colors = colors;
    }

    static Factory(type) {
        switch (type) {
            case 'xiaomi':
                return new PhoneCreat('小米', ['红色', '黑色']);
            case 'oppo':
                return new PhoneCreat('OPPO', ['蓝色', '白色']);
            default:
            return new Error('无法生产该类型的产品');
        }
    }
}

let phone = PhoneCreat.Factory('xiaomi');
console.log(phone);
let phone1 = PhoneCreat.Factory('oppo');
console.log(phone1);

2. 抽象工厂模式

抽象工厂不直接生成实例,我们不生产实例,我们只是实例的搬运工:

// 生产手机
class PhoneCreat {
    constructor(userName, brand, colors) {
        this.userName = userName;
        this.brand = brand;
        this.colors = colors;
    }

    welcome() {
        console.log(`${this.userName},欢迎您使用${this.brand}手机`);
    }

    dataShow() {
        throw new Error('抽象方法不能调用');
    }
}

// 品牌定制功能
class PhoneXM extends PhoneCreat  {
    constructor(name) {
        super(name, '小米', ['red', 'blue', 'green']);
    }

    dataShow() {
        console.log(`您的手机是${this.brand}手机,颜色选择为${this.colors}`); 
    }

    // 红外遥控器
    infrared() {
        console.log('我们的手机可以当遥控其用');
    }
}

class PhoneIPhone extends PhoneCreat  {
    constructor(name) {
        super(name, '苹果', ['black', 'white', 'gray']);
    }

    dataShow() {
        console.log(`您的手机是${this.brand}手机,颜色选择为${this.colors}`); 
    }

    smoth() {
        console.log('我们的手机有流畅的UI体验'); 
    }
}

// 获取抽象工厂
function getAbstractFactory(brand) {
    switch (brand) {
        case 'xiaomi':
            return PhoneXM;
        case 'oppo':
            return PhoneIPhone;
        default:
            throw new Error('没有该品牌手机');
    } 
}

let PhoneClass = getAbstractFactory('xiaomi');
let newPhone = new PhoneClass('张三');
newPhone.welcome();
newPhone.dataShow();

相较于简单工厂,抽象工厂将各个类都抽离出来,可以灵活的维护每个产品类功能增减。

单例模式

单例模式保证一个类只有一个实例,并且提供了访问它的访问点。

// 闭包实现
let Single = (function(){
    let instance;
    class User {
        constructor(name, age) {
            this.name = name;
            this.age = age;
        } 
    }
    return function(name, age) {
        if(!instance) {
            instance = new User(name, age);
        }
        return instance;
    }
})()

let user1 = Single('张三', 18);
let user2 = Single('李四', 20);
console.log(user2) // {name: "张三", age: 18}
console.log(user1 === user2); // true

 // ES6 类写法
class SingleClass {
    constructor(name, age) {
        if(!Single.instance) {
            this.name = name;
            this.age = age;
            Single.instance = this;
        }
        return Single.instance;
    } 
}

let user3 = new SingleClass('王五', 22);
let user4 = new SingleClass('赵六', 24);
console.log(user4) // {name: "王五", age: 22}
console.log(user3 === user4); // true

全局弹窗提示可以使用单例模式,防止多次触发同时生成多个弹窗。

<div id="open">打开</div>
<div id="close">关闭</div>

<script>
    class Modal {
        constructor(msg) {
            if(!Modal.instance) {
                Modal.instance = document.createElement('div');
                Modal.instance.innerText = '网络错误,请检查您的网络。';
                Modal.instance.className = 'modal-content';
                Modal.instance.style.display = 'none';
                document.body.appendChild(Modal.instance);
            }
            return Modal.instance;
        }
    }
    const open = document.getElementById('open');
    open.onclick = () => {
        // 创建
        const modal = new Modal();
        // 显示
        modal.style.display = 'block';
    }
    const close = document.getElementById('close');
    close.onclick = () => {
        const modal = Modal.instance;
        // 隐藏
            modal.style.display = 'none';
    }
</script>

装饰器模式

装饰器模式的核心思想是动态地扩展对象的功能,而不是通过继承来实现。

  • 不修改原对象:装饰器模式通过组合而非继承来扩展对象的功能,遵循了“开闭原则”(对扩展开放,对修改封闭)。
  • 动态扩展:可以在运行时动态地为对象添加功能,而不是在编译时静态地通过继承来实现。
  • 透明性:装饰器对象和被装饰的对象实现相同的接口,因此对客户端代码来说是透明的。

下面是一个监控提交行为的示例,在方法调用前后添加日志记录或性能监控逻辑,而不修改原始方法。

function logDecorator(fn) {
  return function (...args) {
    console.log(`调用函数: ${fn.name}, 参数: ${JSON.stringify(args)}`);
    const result = fn.apply(this, args);
    console.log(`函数返回: ${result}`);
    return result;
  };
}

function submit(info) {
  console.log(`点击了提交事件`);
  return '提交成功'
}

const loggedInfo = logDecorator(submit);
loggedInfo({ name: '123', age: 18 }); // 调用函数: submit, 参数: [{"name":"123","age":18}];点击了提交事件; 函数返回: 提交成功

我们也可以给Function原型上添加前置后置函数,这样可以快速的生成所有方法的装饰器函数:

// Function全局注入前置后置事件
Function.prototype.before = function (fn) {
  const self = this;
  return function () {
    fn.apply(this, arguments);
    return self.apply(this, arguments);
  } 
}

Function.prototype.after = function (fn) {
  const self = this;
  return function () {
    const result = self.apply(this, arguments);
    fn.apply(this, arguments);
    return result;
  } 
}

let test = function () {
  console.log('test事件执行');
}

// 定义装饰器函数
let logTest = test.before(function () {
  console.log('test事件执行前');
}).after(function () {
  console.log('test事件执行后'); 
})
let res = logTest(); // test事件执行前, test事件执行, test事件执行后
console.log("test事件返回值:", res); // test事件返回值: 123

策略模式

该模式主要解决有多种算法相近且多的情况下,使用 if..else 所带来的复杂和难维护。

// 现在我们要对不同的分类进行不同的处理
// 使用 if...else
function calculate(operation, a, b) {
  if (operation === 'add') {
    return a + b;
  } else if (operation === 'subtract') {
    return a - b;
  } else if (operation === 'multiply') {
    return a * b;
  } else {
    throw new Error('不支持的操作');
  }
}

console.log(calculate('add', 2, 3)); // 输出: 5

// 使用策略模式
const strategies = {
  add: (a, b) => a + b,
  subtract: (a, b) => a - b,
  multiply: (a, b) => a * b,
};

function calculateNew(strategy, a, b) {
  if (strategies[strategy]) {
    return strategies[strategy](a, b);
  }
  throw new Error('不支持的操作');
}

console.log(calculateNew('add', 2, 3)); // 输出: 5

该模式对比if...else:

  1. 将每个分支的逻辑封装到独立的策略类中, 避免复杂的条件判断
  2. 可以通过添加新的策略类来扩展功能,而无需修改现有的代码。
  3. 提高代码的可读性和可维护性,每个策略类只关注自己的逻辑,代码结构清晰,易于理解和维护。

代理模式

该模式是对对象生成一个代理对象,通过访问代理对象访问到源对象,可以在代理对象中进行限制处理。

// 假如一个对象需要具体权限才能访问,那么可以设置代理对象进行权限判断是否能访问
let currentRole = 'admin';
class Company {
  constructor() {
    this.baseInfo = '基础信息';
    this.manageInfo = '管理信息';
    this.secretInfo = '机密信息';
  }
  addEmployee() {
    console.log('添加员工');
  }
  deleteEmployee() {
    console.log('开除员工');
  }

}

class ProxyFn {
  constructor() {
    this.superFn = new Company();
  }
  showBaseInfo() {
    console.log(this.superFn.baseInfo); 
  }
  showManageInfo() {
    if (['admin', 'superAdmin'].includes(currentRole)) {
      console.log(this.superFn.manageInfo); 
    } else {
      console.log('没有权限查看'); 
    }
  }
  showSecretInfo() {
    if (currentRole === 'superAdmin') {
      console.log(this.superFn.secretInfo); 
    } else {
      console.log('没有权限查看');
    } 
  }
  addEmployee() {
    if (['admin', 'superAdmin'].includes(currentRole)) {
      this.superFn.addEmployee();
    } else {
      console.log('没有权限招人');
    }
  }
  deleteEmployee() {
    if (currentRole === 'superAdmin') {
      this.superFn.deleteEmployee();
    } else {
      console.log('没有权限开除');
    }
  }
}

const proxyFn = new ProxyFn();
proxyFn.showManageInfo(); // 管理信息
proxyFn.showSecretInfo(); // 没有权限查看


// 可以直接用Proxy代理劫持访问
const proxyCompany = new Proxy(new Company(), {
  get(target, key) {
    if(currentRole === 'superAdmin') {
      return target[key]; 
    } else if(currentRole === 'admin') {
      if(['secretInfo', 'deleteEmployee'].includes(key)) {
        throw new Error('没有权限访问')
      }
      return target[key];
    } else {
      if(['secretInfo', 'deleteEmployee', 'addEmployee'].includes(key)) {
        throw new Error('没有权限访问') 
      }
      return target[key];
    }
  } 
})

console.log(proxyCompany.manageInfo); // 管理信息
console.log(proxyCompany.deleteEmployee()); // Uncaught Error: 没有权限访问

观察者模式

包含观察目标和观察者两个类对象,一个观察目标可以有任意个的观察者类,一旦观察目标发生变化,所有的观察者都将得到通知进行对应的逻辑处理

// 观察目标
class Subject {
  constructor() {
    this.observers = [];
  }

  // 添加观察者
  addObserver(observer) {
    this.observers.push(observer);
  }

  // 通知所有观察者
  notify() {
    this.observers.forEach(observer => observer.update());
  }

  // 移除观察者
  removeObserver(observer) {
    this.observers = this.observers.filter(obs => obs !== observer);
  }
}

// 观察者
class Observer {
  constructor(name) {
    this.name = name;
  }

  update() {
    console.log(`${this.name} 收到通知`);
  }
}

const subject = new Subject();

const observer1 = new Observer('张三');
const observer2 = new Observer('李四');
const observer3 = new Observer('王五');

subject.addObserver(observer1);
subject.addObserver(observer2);
subject.addObserver(observer3);

subject.notify();
// 张三 收到通知
// 李四 收到通知
// 王五 收到通知

观察者模式实现了对象间依赖的低耦合,但却不能对事件通知进行筛选,下面我们来学习它的进阶,发布订阅模式。

发布订阅模式

发布订阅模式包含发布者和订阅者,通过第三方调度通知,能做到谁订阅的就通知谁,属于解耦后的观察者模式。

class Punlish {
  constructor() {
    this.subscribes = {}; // 存放订阅者
  }
  // 发布
  publish(key, value) {
    if(!this.subscribes[key]) {
      throw new Error('没有订阅者');
    }
    this.subscribes[key].forEach(fn => fn(value));
  }

  // 订阅
  subscribe(key, fn) {
    if(!this.subscribes[key]) {
      this.subscribes[key] = [];
    }
    this.subscribes[key].push(fn);
  }

  // 取消订阅
  unsubscribe(key, fn) {
    if(!this.subscribes[key]) {
      throw new Error('没有订阅者');
    }
    const index = this.subscribes[key].indexOf(fn);
    this.subscribes[key].splice(index, 1); // 删除掉该订阅方法
  }

  // 取消一类订阅
  unsubscribeTypeAll(key) {
    delete this.subscribes[key];
  }

  // 取消所有订阅
  unsubscribeAll() {
    this.subscribes = {};
  }

}

const publish = new Punlish();
function a1(value) {
  console.log('a1', value);
}
function a2(value) {
  console.log('a2', value); 
}
function b1(value) {
  console.log('b', value); 
}


publish.subscribe('a', a1)
publish.subscribe('a', a2)
publish.subscribe('b', b1)

publish.publish('a', '我只针对A类发布信息'); // a1 a类订阅 // a2 a类订阅