前端常用设计模式

113 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第4天,点击查看活动详情

什么是设计模式?

设计模式是一组最佳实践的集合,是开发人员面临的一般问题的最佳解决方案。项目中合理运用设计模式可以复用前人的经验,完美解决很多问题。

设计模式原则

设计模式代表的是一种解决问题的思想,其中包含着一些基本原则:

  1. 单一职责原则:一个类只做一件事,功能要单一;
  2. 开放封闭原则:对扩展开放,对修改关闭;
  3. 里式替换原则:基类出现的地方,子类一定要出现;
  4. 接口隔离原则:一个接口应该是一种角色,包含所有该做的事情,排除所有不该做的事情,降低耦合和依赖。
  5. 依赖翻转原则:针对接口编程,依赖抽象而不依赖具体。

常用设计模式

工厂模式

工厂模式是创建一个工厂函数动态进行类的实例化,用于类型无法事先确定,只能动态确定的情况。常用的工厂模式有:简单工厂和工厂方法。

简单工厂

简单工厂是在一个工厂里包含了所有类型的实例创建过程,所以也叫静态工厂,好处是简单,坏处是如果新增了类型,那么就要修改工厂代码,添加新的类型。

interface User {
  name: string;
  role: string;
}

class Admin implements User {
  name: string;
  role: string = 'admin';
  constructor(name: string) {
    this.name = name;
  }
}

class Developer implements User {
  name: string;
  role: string = 'developer';
  constructor(name: string) {
    this.name = name;
  }
}

class UserFactory {
  createUser(name: string, role: string) {
    switch (role) {
      case 'admin':
        return new Admin(name);
      case 'developer':
        return new Developer(name);
      default:
        throw new Error('参数错误');
    }
  }
}

const factory = new UserFactory();
const user: User = factory.createUser('zhangsan', 'developer');
console.log(user); // Developer { role: 'developer', name: 'zhangsan' }

工厂方法

工厂方法就是一个类型对应一个工厂方法,好处是新增类时不需要修改已有工厂的代码,符合开闭原则,坏处是需要为新的类型增加新的工厂函数。在JS中因为原型的存在,我们可以通过把新的类型挂载在工厂原型的方式实现更优的解决方案,无需创建多个工厂。

interface User {
  name: string;
  role: string;
}

class Admin implements User {
  name: string;
  role: string = 'admin';
  constructor(name: string) {
    this.name = name;
  }
}

class Developer implements User {
  name: string;
  role: string = 'developer';
  constructor(name: string) {
    this.name = name;
  }
}

class UserFactory {
  createUser(name: string, role: string) {
    try {
      // @ts-ignore
      return new this[role](name);
    } catch (error) {
      throw new Error('参数错误');
    }
  }
}

// @ts-ignore
UserFactory.prototype['admin'] = Admin;
// @ts-ignore
UserFactory.prototype['developer'] = Developer;

const factory = new UserFactory();
const user: User = factory.createUser('zhangsan', 'developer');
console.log(user); // Developer { role: 'developer', name: 'zhangsan' }

构造器模式

在面向对象的编程语言中,构造器是一个类中用来初始化新对象的特殊方法,可以接受参数用来设定实例对象的属性和方法。JS中通过函数和原型实现了构造器模式。

function User(name, role) {
  this.name = name;
  this.role = role;
}

User.prototype.getName = function () {
  return this.name;
};

const user = new User('zhangsan', 'admin');
const userName = user.getName();
console.log(userName);

单例模式

单例模式的定义是:保证一个类仅有一个实例,并提供一个访问它的全局访问点。如果我们希望某些对象在全局只被创建一次,比如全局缓存、window对象等,那我们就可以使用单例模式。

function getSingleton(Func) {
  let instance;
  return function () {
    if (!instance) {
      instance = new Func(...arguments);
    }
    return instance;
  };
}

function User(name, role) {
  this.name = name;
  this.role = role;
}

const UserSingleton = getSingleton(User);
const user1 = UserSingleton('zhangsan', 'admin');
const user2 = UserSingleton('lisi', 'developer');
console.log(user1 === user2); // true

原型模式

用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。JS中通过Object.create()实现原型模式,将现有对象绑定至新创建对象的__proto__。

let user = {
  name: 'default',
  getName() {
    return this.name;
  },
};

let admin = Object.create(user, {
  name: {
    value: 'zhangsan',
  },
});

console.log(admin.getName()); // zhangsan

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

发布订阅模式,又叫观察者模式,它定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。JS中的事件模型就是一种典型的发布订阅模式。发布订阅模式一般由依赖收集和依赖触发两个部分构成,比如Vue中的响应式原理。

class Observer {
  constructor() {
    this.subscribeList = [];
  }

  publish(type, ...args) {
    const subFns = this.subscribeList[type];
    subFns.forEach((fn) => {
      fn.apply(this, args);
    });
  }

  subscribe(type, fn) {
    if (!this.subscribeList[type]) {
      this.subscribeList[type] = [];
    }
    this.subscribeList[type].push(fn);
  }

  remove(type, fn) {
    if (type === 'undefined') {
      this.subscribeList = [];
      return;
    }

    if (!this.subscribeList[type]) {
      return;
    }

    const subFns = this.subscribeList[type];
    if (fn === 'undefined') {
      subFns = [];
      return;
    }

    for (let i = 0; i < subFns.length; i++) {
      if (subFns[i] === fn) {
        subFns.splice(i, 1);
      }
    }
  }
}

const ob = new Observer();

ob.subscribe('event', function () {
  console.log('event first sub');
});
ob.subscribe('event', function () {
  console.log('event second sub');
});

ob.publish('event');

适配器模式

适配器模式用于解决两个软件实体间接口不兼容的问题,比如一个函数只接受数组入参,另一个函数只返回类数组对象,那么我们就需要定义一个适配器将类数组对象转成数组以实现接口之间的兼容。适配器也叫包装器,目的是将同一个核心功能以不同的形式兼容多种用户界面。

const data = {
  0: 'a',
  1: 'b',
  2: 'c',
};

function renderArr(arr) {
  arr.forEach((item) => console.log(item));
}

function arrayAdapter(data) {
  if (typeof data !== 'object' || data === null) {
    throw new Error('data must be object');
  }
  const arr = [];
  for (key in data) {
    if (data.hasOwnProperty(key)) {
      arr.push(data[key]);
    }
  }
  return arr;
}

renderArr(arrayAdapter(data));

装饰器模式

在程序运行期间动态地给某个对象添加一些额外功能,而不会影响对象本身,经过多重包装可以形成一条装饰链。

class Girl {
  faceValue() {
    console.log('我原本的脸');
  }
}
class ThinFace {
  constructor(girl) {
    this.girl = girl;
  }
  faceValue() {
    this.girl.faceValue();
    console.log('开启瘦脸');
  }
}
class IncreasingEyes {
  constructor(girl) {
    this.girl = girl;
  }
  faceValue() {
    this.girl.faceValue();
    console.log('增大眼睛');
  }
}
let girl = new Girl();
girl = new ThinFace(girl);
girl = new IncreasingEyes(girl);
girl.faceValue();
/**
我原本的脸
开启瘦脸
增大眼睛
*/

代理模式

当客户不方便直接访问一个对象或者对象不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象。替身对象对请求做出一些处理之后,再把请求转交给原始对象。代理模式分为三种:保护代理、虚拟代理、缓存代理。

保护代理

保护代理用于限制原始对象的访问,比如鉴权、参数检查等等。

function sendMessage(msg) {
  console.log(msg);
}

function sendMessageProxy(msg) {
  if (msg === undefined) {
    throw new Error('msg 不能为空');
  }
  msg = msg + '';
  msg = msg.replace(/敏感词汇/g, '');
  sendMessage(msg);
}

sendMessageProxy('敏感词汇hello'); // hello

虚拟代理

虚拟代理用于在访问原始对象前添加一些额外的操作,比如函数防抖等。

function sendMessage(msg) {
  console.log(msg);
}

function debounce(fn, delay) {
  delay = delay || 200;
  let timer;
  return function () {
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, arguments);
    }, delay);
  };
}

const fn = debounce(sendMessage, 1000);
fn('hello');
fn('hello');
fn('hello');

缓存代理

缓存代理可以为一些开销比较大的运算结果提供暂时的缓存,提升效率。

function add() {
  console.log('add trigger');
  const arg = Array.from(arguments);
  return arg.reduce((pre, cur) => {
    return pre + cur;
  }, 0);
}

function addProxy() {
  const cache = {};
  return function () {
    const arg = Array.from(arguments);
    const argStr = arg.join(',');
    if (cache[argStr]) {
      return cache[argStr];
    }
    return (cache[argStr] = add.apply(this, arg));
  };
}

const fn = addProxy();
console.log(fn(1, 2, 3)); // add trigger 6
console.log(fn(1, 2, 3)); // 6

外观模式

为子系统中的一组接口提供一个统一的界面,定义一个高层接口,这个接口使子系统更加容易使用。外观模式在JS中可以认为是一组函数的集合。

// 三个处理函数
function start() {
  console.log('start');
}

function doing() {
  console.log('doing');
}

function end() {
  console.log('end');
}

// 外观函数,将一些处理统一起来,方便调用
function execute() {
  start();
  doing();
  end();
}

execute();

迭代器模式

迭代器模式是指提供一种方法顺序访问一个集合对象中的各个元素,而无需暴露集合对象的内部细节。在使用迭代器模式之后,即使不知道对象的内部构造,也可以按顺序访问其中的某个元素。

function each(obj, cb) {
  if (typeof obj !== 'object' || obj === null) {
    throw new Error('obj must be a object');
  }
  if (Array.isArray(obj)) {
    obj.forEach((item, index) => {
      cb(item, index);
    });
  } else {
    for (const key in obj) {
      cb(obj[key], key);
    }
  }
}

const cb = function (item) {
  console.log(item);
};
const obj1 = [1, 2, 3];
const obj2 = { name: 'zhangsan', age: 18 };
each(obj1, cb); // 1 2 3
each(obj2, cb); // zhangsan 18