常见 JavaScript 设计模式 — 原来这么简单

设计模式总共有 23 种,但在前端领域其实没必要全部都去学习,毕竟大部分的设计模式是在 JavaScript 中占的比重并不是那么大,本文会列举出一些 JavaScript 常见的、容易被忽视的设计模式,不过还是有必要先简单了解一下设计模式相关的概念.设计模式是什么?先举个形象的例子,比如现在正在考试而且恰好在考数学,实际上每道数学题目都对应着一种或多种解决公式(如和三角形相关的勾股定理),而这些解决公式是经过数学家研究、推导、总结好的,我们只需要把 题目 和 已有公式 对应上就很容易解决问题,而 设计模式 也是如此,只不过是它是相对于 软件设计领域 而言的.设计模式(Design pattern) 是一套被反复使用、经过分类、代码设计经验的总结,简单来说设计模式就是为了解决 软件设计领域 不同场景下相应问题的 解决方案.设计原则(SOLID)SOLID 实际上指的是五个基本原则,但在前端领域涉及到最多的是仍然是前面两条:单一功能原则(Single Responsibility Principle)开放封闭原则(Opened Closed Principle)里式替换原则(Liskov Substitution Principle)接口隔离原则(Interface Segregation Principle)依赖反转原则(Dependency Inversion Principle)设计模式的类型主要分为三个类型:创建型主要用于解耦 对象的实例化 过程,即用于创建对象,如对象实例化本文主要包含:简单工厂模式、抽象工厂模式、单例模式、原型模式行为型主要用于优化不同 类、对象、接口 间的结构关系,如把 类 或 对象 结合在一起形成一个更大的结构本文主要包含:装饰器模式、适配器模式、代理模式结构型主要用于定义 类 和 对象 如何交互、划分责任、设计算法本文主要包含:策略模式、状态模式、观察者模式、发布订阅模式、迭代器模式创建型设计模式设计模式的核心是区分逻辑中的 可变部分 和 不变部分,并使它们进行分离,从而达到使变化的部分易扩展、不变的部分稳定.工厂模式简单工厂模式核心就是创建一个对象,这里的 可变部分 是 参数,不变部分 是 共有属性.举例:通过不同职级的员工创建员工相关信息,需要包含 name、age、position、job 等信息.实现方式一:核心就是 可变部分 默认 参数化function Staff(name, age, position, job) {
this.name = name;
this.age = age;
this.position = position;
this.job = job;
}

const developer = new Staff('zs', 18, 'develoment', ['写 bug', '改 bug', '摸鱼']);
const productManager = new Staff('ls', 30, 'manager', ['提需求', '改需求', '面向 PPT 开发']);实现方式二:实际上在实现方式一中的 job 部分是和 position 是相互关联的,可以认为 job 部分是 不变的,因此可以根据 position 内容的内容来自动匹配 jobfunction Staff(name, age, position, job) {
this.name = name;
this.age = age;
this.position = position;
this.job = job;
}

function StaffFactory(name, age, position){
let job = []
switch (position) {

  case 'develoment':
      job = ['写 bug', '改 bug', '摸鱼'];
      break;
  case 'manager':
      job = ['提需求', '改需求', '面向 PPT 开发'];
      break;
  ...

}

return new Staff(name, age, position, job);
}

const developer = StaffFactory('zs', 18, 'developer');
const productManager = StaffFactory('ls', 30, 'manager');抽象工厂模式这个模式最显眼的就是 抽象 两个字了,在如 Java 语言当中存在所谓的 抽象类,这个抽象类里面的所有属性和方法都没有具体实现,只有单纯的定义,而继承这个抽象类的子类必须要实现其对应的抽象属性和抽象方法.在 JavaScript 中没有这样的直接定义,不过根据上面的描述其实我们可以把它映射到 typescript 中的 interface 接口,理解到这其实让我联想到了 vue.js 中的 自定义渲染器,预留的自定义渲染器的各个方法目的就是实现跨平台的渲染方式// 文件位置:packages\runtime-core\src\renderer.ts
export function createRenderer<
HostNode = RendererNode,
HostElement = RendererElement

(options: RendererOptions) {
return baseCreateRenderer(options)
}

// 文件位置:packages\runtime-core\src\renderer.ts
// RendererOptions 就是一个 Interface 接口
export interface RendererOptions<
HostNode = RendererNode,
HostElement = RendererElement

{
patchProp(
el: HostElement,
key: string,
prevValue: any,
nextValue: any,
isSVG?: boolean,
prevChildren?: VNode[],
parentComponent?: ComponentInternalInstance | null,
parentSuspense?: SuspenseBoundary | null,
unmountChildren?: UnmountChildrenFn

): void
insert(el: HostNode, parent: HostElement, anchor?: HostNode | null): void
remove(el: HostNode): void
createElement(

type: string,
isSVG?: boolean,
isCustomizedBuiltIn?: string,
vnodeProps?: (VNodeProps & { [key: string]: any }) | null

): HostElement
createText(text: string): HostNode
createComment(text: string): HostNode
setText(node: HostNode, text: string): void
setElementText(node: HostElement, text: string): void
parentNode(node: HostNode): HostElement | null
nextSibling(node: HostNode): HostNode | null
querySelector?(selector: string): HostElement | null
setScopeId?(el: HostElement, id: string): void
cloneNode?(node: HostNode): HostNode
insertStaticContent?(

content: string,
parent: HostElement,
anchor: HostNode | null,
isSVG: boolean,
start?: HostNode | null,
end?: HostNode | null

): [HostNode, HostNode]
}接下来我们将以上的 typescript 的形式转变成 JavaScript 形式的抽象模式:// 抽象 Render 类
class Renderer {
patchProp(

el,
key,
prevValue,
nextValue,
isSVG,
prevChildren,
parentComponent,
parentSuspense,
unmountChildren

) {

throw Error('抽象工厂方法不能直接使用,你需要将我重写!!!');

}
insert(el, parent, anchor) {

throw Error('抽象工厂方法不能直接使用,你需要将我重写!!!');

}
remove(el) {

throw Error('抽象工厂方法不能直接使用,你需要将我重写!!!');

}
createElement(type, isSVG, isCustomizedBuiltIn, vnodeProps) {

throw Error('抽象工厂方法不能直接使用,你需要将我重写!!!');

}
createText(text) {

throw Error('抽象工厂方法不能直接使用,你需要将我重写!!!');

}
createComment(text) {

throw Error('抽象工厂方法不能直接使用,你需要将我重写!!!');

}
setText(node, text) {

throw Error('抽象工厂方法不能直接使用,你需要将我重写!!!');

}
setElementText(node, text) {

throw Error('抽象工厂方法不能直接使用,你需要将我重写!!!');

}
parentNode(node) {

throw Error('抽象工厂方法不能直接使用,你需要将我重写!!!');

}
nextSibling(node) {

throw Error('抽象工厂方法不能直接使用,你需要将我重写!!!');

}
querySelector(selector) {

throw Error('抽象工厂方法不能直接使用,你需要将我重写!!!');

}
setScopeId(el, id) {

throw Error('抽象工厂方法不能直接使用,你需要将我重写!!!');

}
cloneNode(node) {

throw Error('抽象工厂方法不能直接使用,你需要将我重写!!!');

}
insertStaticContent(content, parent, anchor, isSVG, start, end) {

throw Error('抽象工厂方法不能直接使用,你需要将我重写!!!');

}
}

// 具体渲染函数的实现
class createRenderer extends Renderer{

// 待实现的渲染器方法
...

}单例模式核心就是通过多次 new 操作进行实例化时,能够保证创建 实例对象 的 唯一性.vuex 中的单例模式其实,vuex 中就使用到了 单例模式,代码本身比较简单,当 install 方法被多次调用时,就会得到一个错误信息,并不会多次向 Vue 中混入 vuex 中自定义的内容:
图片

图片
实现一个单例模式这里举个封装 localStorage 方法的例子,并提供给外部对应的创建方法,如下:let storageInstance = null;

class Storage {

getItem(key) {
    let value = localStorage.getItem(key);
    try {
        return JSON.parse(value);
    } catch (error) {
        return value;
    }
}

setItem(key, value) {
    try {
        localStorage.setItem(JSON.stringify(value));
    } catch (error) {
        // do something
        console.error(error);
    }
}

}

// 单例模式
export default function createStorage(){

if(!storageInstance){
    storageInstance = new Storage();
}
return storageInstance;

}原型模式在 JavaScript 中原型模式是很常见的,JavaScript 中实现的 继承 或者叫 委托 也许更合适,因为它不等同于如 Java 等语言中的继承,毕竟 JavaScript 的 继承 是基于原型(prototype)来实现.class Person {

say() {
    console.log(`hello, my name is ${this.name}!`);
}

eat(foodName) {
    console.log(`eating ${foodName}`);
}

}

class Student extends Person {

constructor(name) {
    super();
    this.name = name;
}

}

const zs = new Student('zs');
const ls = new Student('ls');

console.log(zs.say === ls.say);// Java 中是不相等的, JavaScript 中是相等的
console.log(zs.eat === ls.eat);// Java 中是不相等的, JavaScript 中是相等的vue2 中的原型模式文件位置:\src\core\instance\lifecycle.js
图片
结构型设计模式装饰器模式核心是在不改变原 对象/方法 的基础上,通过对其进行包装拓展,使原有 对象/方法 可以满足更复杂的需求.装饰器本质装饰器模式本质上就是 函数的传参和调用,通过函数为已有 对象/方法 进行扩展,而不用修改原对象/方法,满足 开放封闭原则.通过配置 babel 通过将 test.js 转为为 bable_test.js 用来查看装饰器的本质:babel.config.json{
"presets": [

[
  "@babel/preset-env",
  {
    "targets": {
      "node": "current"
    }
  }
]

],
"plugins": [

["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-class-properties", { "loose": true }]

]
}test.js// 定义装饰器
function decoratorTest(target) {
console.log(target);
}

// 使用装饰器,装饰 Person 类
@decoratorTest
class Person {
say() {}
eat() {}
}执行 babel test.js --out-file babel_test.js 命令是生成 babel_test.js"use strict";

var _class;

function decoratorTest(target) {
console.log(target);
}

let Person = decoratorTest(_class = class Person {
say() {}

eat() {}

}) || _class;React 中的装饰器模式 —— HOC 高阶组件高阶组件 是参数为 组件,返回值为新组件的 函数,在 React 中 HOC 通常用于复用组件公共逻辑.// TodoList 组件
class TodoList extends React.Component {}

// HOC 函数
function WrapContainer(Comp) {
return (

);
}

// HOC 装饰 TodoList 组件,为 TodoList 组件包裹红色边框
const newTodoList = WrapContainer(TodoList);适配器模式适配器模式本质就是 让原本不兼容的功能能够生效,避免大规模修改代码,对外提供统一使用.Axios 中的适配器通过观察 Axios 的目录结构,很容就发现其使用了适配器模式:
图片
其实 Axios 中的 adapters 主要目的是根据当前运行时环境,向外返回对应的适配器 adapter,而这个适配器要做的其实就是兼容 web 浏览器环境和 node 环境的 http 请求,保证对外暴露的仍然是统一的 API 接口
图片
代理模式代理模式顾名思义就是 不能直接访问目标对象,需要通过代理器来实现访问,通常是为了提升性能、保证安全等.事件代理事件代理是很常见的性能优化手段之一,react 的事件机制也采用了事件代理的方式(篇幅有限可自行了解),这里演示简单的 JavaScript 事件代理:


this number is 1


this number is 2


this number is 3


this number is 4


this number is 5


Vue 中的代理 ProxyVue.js 3 中通过 Proxy 实现了对数据的代理,任何读取、设置的操作都会被 代理对象 的 handlers 拦截到,从而实现 Vue 中的 track 和 trigger
图片

图片
行为型设计模式策略模式策略模式实际上就是定义一系列的算法,将单个功能封装起来,并且对扩展开放.举个例子假如我们需要为某个游乐场的门票价格做差异化询价,主要人员类型分为 儿童、成年人、老年人 三种,其对应的门票折扣为 8折、9折、8.5折if-else 代码一把梭缺点:无论哪种人员类型的折扣变动,都需要修改 finalPrice 函数,不符合对 对修改封闭function finalPrice(type, price) {
if (type === "child") {

// do other thing
return price * 0.8;

}

if (type === "adult") {

// do other thing
return price * 0.9;

}

if (type === "aged") {

// do other thing
return price * 0.85;

}
}单一功能封装缺点:若人员类型增加妇女类型,仍然需要修改 finalPrice 函数,且不符合 对扩展开放function childPrice(price) {
// do other thing
return price * 0.8;
}

function adultPrice(price) {
// do other thing
return price * 0.9;
}

function agedPrice(price) {
// do other thing
return price * 0.85;
}

function finalPrice(type, price) {
if (type === "child") {

return childPrice(price);

}

if (type === "adult") {

return adultPrice(price);

}

if (type === "aged") {

return agedPrice(price);

}
}创建映射关系通过映射关系,很好的将 finalPrice 和 具体的计算逻辑进行分离,在需要扩展类型时,只需要修改 priceTypeMap 对象而不用修改对外暴露的 finalPrice 函数.const priceTypeMap = {
child: function (price) {

// do other thing
return price * 0.8;

},
adult: function (price) {

// do other thing
return price * 0.9;

},
aged: function (price) {

// do other thing
return price * 0.85;

},
};

function finalPrice(type, price) {

return priceTypeMap[type](price);

}状态模式状态模式允许一个对象在其内部状态发生改变时,能够改变原本的行为.举例子假如现在我们需要设计一个售票机器,主要出售 巴士、火车、飞机票等,价格分别为 50、150、1000,并且能够根据剩余票数决定是否能够继续购买.通过策略模式实现核心代码逻辑有了上面的 策略模式 的思想,立马就可以设计出如下的代码:缺点:没有根据剩余票数决定是否可以继续售卖,主要原因就在于抽离的 ticketTypeMap 和 TicketMachine 之间的状态没有关联const ticketTypeMap = {
bus() {

// do other thing
return 50;

},
train() {

// do other thing
return 150;

},
plane() {

// do other thing
return 1000;

},
};

class TicketMachine {
constructor() {

// 剩余票数
this.remain = {
  bus: 100,
  train: 150,
  plane: 200,
};

}

selling(type) {

return ticketTypeMap[type]();

}
}关联对象状态 — 函数传参通过函数传参的方式将对象传递给目标函数,让目标函数通过该对象访问和修改对象内部的状态.const ticketTypeMap = {
bus(remain) {

if (remain.bus <= 0) return Error("抱歉,巴士票已售完");
remain.bus--;
return 50;

},
train(remain) {

if (remain.train <= 0) return Error("抱歉,火车票已售完");
remain.train--;
return 150;

},
plane(remain) {

if (remain.plane <= 0) return Error("抱歉,飞机票已售完");
remain.plane--;
return 1000;

},
};

class TicketMachine {
constructor() {

// 剩余票数
this.remain = {
  bus: 100,
  train: 150,
  plane: 200,
};

}

selling(type) {

return ticketTypeMap[type](this.remain);

}
}关联对象状态 — 整合方法实际上 ticketTypeMap 映射的方法和 TicketMachine 有较强的关联性,不应该单独存在,因此,可以将这个映射对象整合进 TicketMachine 当中class TicketMachine {
constructor() {

// 剩余票数
this.remain = {
  bus: 100,
  train: 150,
  plane: 200,
};

}

ticketTypeMap = {

that: this,
bus() {
  const { remain } = this.that;
  if (remain.bus <= 0) return Error("抱歉,巴士票已售完");
  remain.bus--;
  return 50;
},
train() {
  const { remain } = this.that;
  if (remain.train <= 0) return Error("抱歉,火车票已售完");
  remain.train--;
  return 150;
},
plane() {
  const { remain } = this.that;
  if (remain.plane <= 0) return Error("抱歉,飞机票已售完");
  remain.plane--;
  return 1000;
},

};

selling(type) {

return this.ticketTypeMap[type]();

}
}观察者模式观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个目标对象,当这个目标对象的状态发生变化时,会通知所有观察者对象,使它们能够自动更新.vue 中的观察者模式

你可能感兴趣的:(javascript)