参考
- JavaScript 设计模式核⼼原理与应⽤实践
- 「进击的前端工程师」修炼内功之 JavaScript 设计模式(一)
- 「进击的前端工程师」修炼内功之 JavaScript 设计模式(二)
- 「进击的前端工程师」修炼内功之 JavaScript 设计模式(三)
概念
小明在玩云顶之奕时,研究出一种策略:前期买低费棋子打工,等升到 6 级后,疯狂刷新寻找暗星棋子,凑 6 暗星羁绊。他玩了很多把后,发现这个策略非常管用,基本每局都能进前 3。乐于分享的他将这个策略总结了下来,并放到网上与其他玩家分享。大家照着他的策略愉快地玩耍,久而久之,这种策略已然成为了一种固有套路。
程序员在写代码时,也有着一些固定的套路,按照某某套路写出来的代码,往往比较清晰、健壮。而这些套路,其实就是所谓的设计模式。
创建型
构造器模式
// 你作为拳头的程序员
// 被要求用JavaScript重写LOL
// 首先,来一个快乐风男
const Yasuo = {
name: "亚索",
title: "疾风剑豪",
location: "战士",
};
// 接下来,再来一个寒冰
const Ashe = {
name: "艾希",
title: "寒冰射手",
location: "射手",
};
// 你想到,还有100多个英雄要写
// 如果继续这样写,还得再写一百多个对象字面量
// 不如写个构造函数吧
function Hero(name, title, location) {
this.name = name;
this.title = title;
this.location = location;
}
// 好了,这样添加英雄省事多啦
const Jinx = new Hero("金克丝", "暴走萝莉", "射手");
简单工厂模式
// 设计师跟你说,不同定位的英雄应该有不同的特质
// 比如射手是移动的atm,刺客是ad去质器
// 于是你把Hero拆分成了Shooter和Assassin
function Shooter(name, title, location) {
this.name = name;
this.title = title;
this.location = "射手";
this.speciality = "移动的ATM";
}
function Assassin(name, skills) {
this.name = name;
this.title = title;
this.location = "刺客";
this.speciality = "AD去质器";
}
// 然后通过函数来判别英雄定位和分配构造函数
function handleLocation(name, title, location) {
switch (location) {
case "射手":
return new Shooter(name, title, location);
break;
case "刺客":
return new Assassin(name, title, location);
break;
}
}
const Caitlyn = handleLocation("凯特琳", "皮城女警", "射手");
const Zed = handleLocation("劫", "影流之主", "刺客");
// 但你思考了下,其实hooter和Assassin都有着共同的属性
// 而且location和speciality的值是统一的
// 即speciality的值是根据location的值决定的
// 为了更加彻底地封装共性和分离出个性,你又把他们整合回了Hero
function Hero(name, title, location, speciality) {
this.name = name;
this.title = title;
this.location = location;
this.speciality = speciality;
}
// 然后写一个生成Hero的函数
// 因为Hero们就像是从流水线上生产的产品
// 所以你给这个函数取名为HeroFacroty
function HeroFactory(name, title, location) {
let speciality;
switch (location) {
case "射手":
speciality = "移动的ATM";
break;
case "刺客":
speciality = "AD去质器";
break;
}
return new Hero(name, title, location, speciality);
}
const Caitlyn = HeroFactory("凯特琳", "皮城女警", "射手");
const Zed = HeroFactory("劫", "影流之主", "刺客");
抽象工厂模式
// 写完英雄后,该写游戏模式了
// 首先,总结下游戏模式有哪些属性
function Mode(type, map, mechanism) {
this.type = type; // 类型
this.map = map; // 地图
this.mechanism = mechanism; // 机制
}
// 模仿HeroFactory,写ModeFactory
function ModeFactory(type) {
let map, mechanism;
switch (type) {
case "极地大乱斗":
map = "...";
mechanism = "...";
break;
case "无限火力":
map = "...";
mechanism = "...";
break;
case "克隆大作战":
map = "...";
mechanism = "...";
break;
default:
map = "...";
mechanism = "...";
break;
}
return new Mode(type, map, mechanism);
}
const ultraRapidFire = ModeFactory("无限火力");
// 看起来真是岁月静好
// 直到某天,设计师开始整活了
// 他提出给元素龙加一条“吴奇龙”
// 于是你改写了关于召唤师峡谷地图的代码
// 几天后,设计师又让你再加一条“史泰龙”
// 你不得已再一次改写了一次代码
// 数次改写导致ModeFactory愈发混乱、臃肿,难以维护
// 你突发奇想,不如也抽象出关于map和mechanism的工厂
function MapFactory(type) {
switch (type) {
case "召唤师峡谷":
// 处理召唤师峡谷的逻辑代码,其他地图同理
case "嚎哭深渊":
case "屠夫之桥":
case "飞升之地":
}
}
function MechanismFactory(type) {
// ...
}
// 如此一来,如果要改动召唤师峡谷
// 就只需要改动MapFactory中对应的那一部分
// 但如果地图种类繁多、逻辑复杂
// MapFactory还是会出现和ModeFactory一样的问题
// 所以我们可以把各个地图再独立出来
// (MechanismFactory同理)
function MapSummonersRift() {
// 仅存放召唤师峡谷相关的代码
}
function MapHowlingAbyss() {
// 仅存放嚎哭深渊相关的代码
}
function MapFactory(type) {
switch (type) {
case "召唤师峡谷":
return new MapSummonersRift();
case "嚎哭深渊":
return new MapHowlingAbyss();
}
}
// 如此一来,工厂也有层级之分了
// ModeFactory是管理着MapFactory和MechanismFactory的超级工厂
// MapFactory下面又管理者MapSummonersRift、MapHowlingAbyss等
function ModeFactory(type) {
let map, mechanism;
switch (type) {
case "极地大乱斗":
map = MapFactory("嚎哭深渊");
mechanism = MechanismFactory("...");
break;
default:
map = MapFactory("召唤师峡谷");
mechanism = MechanismFactory("...");
break;
}
return new Mode(type, map, mechanism);
}
// 日子再次回归于岁月静好,直到某天,策划出了个皮尔特沃夫限定活动
// 极地大乱斗的嚎哭深渊需要改成屠夫之桥,但游戏机制不变
// 你不得不回头修改ModeFactory中type的判别逻辑(而且再活动结束后,还得再改回来)
// 这种对“已建工厂”的修改违反了开放封闭原则(类、模块、函数可以扩展,但是不可修改的原则)
// 你思考着,如何避免直接修改工厂内部的逻辑呢?不如试试将Mode像Map那样独立出来
function ModeDefault() {
this.map = new MapSummonersRift();
this.mechansims = new MechansismsDefault();
}
function ModePolar() {
this.map = new MapHowlingAbyss();
this.mechansims = new MechansismsPolar();
}
const ModeDefaultInstance = new ModeDefault();
const ModePolarInstance = new ModePolar();
// 新模式的添加不会影响已有模式
function ModePiltover() {
this.map = new MapButchersBridge();
this.mechansims = new MechansismsPolar();
}
const ModePiltoverInstance = new ModePiltover();
// 然而这样做,产生了一个新的问题
// 对于Mode们有哪些属性、方法,没有成文的规定
// 机智的你从Java里的抽象类中获取了灵感,
// 借用“抽象工厂”和”抽象产品“来规范mode、map、mechansims格式
class ModeFactory {
createMap() {
throw new Error("抽象工厂方法不允许直接调用,你需要将我覆写!");
}
createMechansims() {
throw new Error("抽象工厂方法不允许直接调用,你需要将我覆写!");
}
}
class Map {
init() {
throw new Error("抽象工厂方法不允许直接调用,你需要将我覆写!");
}
}
class Mechansims {
init() {
throw new Error("抽象工厂方法不允许直接调用,你需要将我覆写!");
}
}
// 下面是具体的map(具体的mechansims略)
class MapSummonersRift extends Map {
init() {
//
}
}
class MapHowlingAbyss extends Mechansims {
init() {
//
}
}
// 具体的mode
class ModeDefaultFactory extends ModeFactory {
createMap() {
return new MapSummonersRift();
}
createMechansims() {
return new MechansismsDefault();
}
}
const ModeDefaultInstance = new ModeDefaultFactory();
// 下面我们来总结一下:
// ModeFactory,抽象工厂:声明最终目标产品的共性、规范
// ModeDefaultFactory,具体工厂:继承自抽象工厂,实现了抽象工厂里声明的方法,用于实例化最终目标产品
// Map,Mechansims,抽象产品:具体工厂里实现的方法,会依赖一些产品,抽象产品声明这些产品的共性、规范
// MapSummonersRift,MapHowlingAbyss,具体产品:见抽象产品
单例模式
// 游戏内需要添加一个用于记录对局中数据的对象
// 而且这个对象由十个玩家共用
class GameInfo {
//
}
// 在玩家1这边:
const gameInfo1 = new GameInfo();
// 在玩家2这边:
const gameInfo2 = new GameInfo();
// 然而玩家1和玩家2并没有共有同一个对象
gameInfo1 === gameInfo2; // false
// 我们为GameInfo添加静态方法
// 以保证多次调用getInstance获取的是同一对象
class GameInfo {
static getInstance() {
if (!GameInfo.instance) {
GameInfo.instance = new GameInfo();
}
return GameInfo.instance;
}
}
const gameInfo1 = GameInfo.instance();
const gameInfo2 = GameInfo.instance();
gameInfo1 === gameInfo2; // true
原型模式
// 你应该很熟悉构造函数、原型对象、实例、原型链等概念了
// 而在JavaScript中,原型编程范式的体现就是基于原型链的继承
// 下面总结一个常见的面试题:实现深拷贝
// 方法一,缺点是没法拷贝函数和正则
function deepClone(obj) {
let str = JSON.stringify(obj);
return JSON.parse(str);
}
// 方法二,利用递归,从原理层面实现了深拷贝
function deepClone(obj) {
// 如果obj是null或基本类型,直接返回
if (obj === null || typeof obj !== "object") {
return obj;
}
// 初始化复制到的值
let value;
if (obj instanceof Array) {
copy = [];
} else {
copy = {};
}
// 遍历obj
for (let key in obj) {
// 如果key是obj的自有属性
if (obj.hasOwnProperty(key)) {
// 递归调用深拷贝方法
copy[key] = deepClone(obj[key]);
}
}
return copy;
}
结构型
装饰器模式
// 想象这里有一坨屎山,它虽然实现了业务功能,但代码乱七八糟
var falsebbb = "rld",
aaatrue = "hel",
shit = "lo wo",
dsadq2 = aaatrue + shit + falsebbb;
// 我们把屎山的逻辑抽离出来,包装成函数
function oldShit() {
// 屎山,也就是上面的代码
}
// 老板让我们加一个新功能,我们把新功能写在新函数(装饰器)里
function newShit() {
// 自己写的新屎山
}
// 把新旧功能合并包装一下
function moreShit() {
oldShit();
newShit();
}
// 这种“只添加,不修改”的模式就是装饰器模式,新屎旧屎互不影响
适配器模式
// 你写了个好用的函数
function newAndGood(params) {
let { sex, name, age } = params;
// 业务逻辑
}
// 公司有段陈年代码
function oldAndBad(name, sex, age) {
// 业务逻辑
}
// 老板让你升级一下旧代码,于是你写了个适配器
function OldApatar(params) {
newAndGood(params);
}
function oldAndBad(name, sex, age) {
oldApatar({ sex, name, age });
}
// 适配器类似于转换器,譬如typec转接口
代理模式
// 代理,类似于房产中介、fanqiang软件,等等
// ES6中新增了代理器Proxy
const proxy = new Proxy(obj, handler);
// 假设你在b站看片,b站对vip影视库设置了保护代理
const videos = {
free: ["绝代双骄", "你的名字"],
vip: ["天气之子", "逃避虽可耻但有用"],
};
let isVIP = false,
isAdmin = false;
const bilibiliVideos = new Proxy(videos, {
get(obj, key) {
if (!isVIP && key === "vip") {
console.log("不充钱不给看!");
return;
} else {
return obj[key];
}
},
set(obj, key, val) {
if (!isAdmin) {
// 这里有个bug,只限制了赋值操作,但仍能通过push等方法修改数组
// (可以通过添加push代理/拦截器的方法解决这个bug)
console.log("非管理员无权修改视频库!");
return;
} else {
obj[key] = val;
}
},
});
// 事件委托也属于代理模式,父元素代理子元素
const father = document.getElementById("father");
father.addEventListener("click", function (event) {
event.preventDefault();
switch (event.target) {
case "child1":
//
case "child2":
//
}
});
// 图片预加载亦是,当图片过大时,先给节点设置一个占位图
// 图片资源绑定到不可见节点上,当图片加载完成,再绑定到原先节点
class PreLoadImage {
// 初始化节点
constructor(node) {
this.node = node;
}
// 设置src
setSrc(imageUrl) {
this.node.src = imageUrl;
}
}
class ProxyImage {
// 占位图地址
static LOADING_URL = "xxxxxx";
// 初始化代理资源
constructor(image) {
this.image = image;
}
// 代理设置src
setSrc(imageUrl) {
this.image.setSrc(ProxyImage.LOADING_URL);
// 设置虚拟节点
const virtualImage = new Image();
virtualImage.src = imageUrl;
virtualImage.onload = () => {
this.image.setSrc(imageUrl);
};
}
}
let node = document.getElementById("img"),
image = new PreloadImage(node),
proxy = new ProxyImage(image);
proxy.setSrc("图片url");
// 缓存代理
// 求和方法
const sum = (nums) => {
let length = nums.length,
result = 0;
for (let i = 0; i < length; i++) {
result += nums[i];
}
return result;
};
// 为求和方法创建代理
const proxySum = (function () {
// 求和结果的缓存池
let cache = {};
return function () {
// 将参数字符串作为标识符
let nums = [...arguments],
key = nums.join(",");
return key in cache ? cache[key] : (cache[key] = sum(nums));
};
})();
// b会直接从缓存池中取值
let a = proxySum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10),
b = proxySum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
行为型
策略模式
// 策略模式通过定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换
// 换句话说,就是把if-else结构转化为一个或一系列的映射结构
function getData(account) {
let data;
switch (account) {
case "admin":
// data = 获取管理员相关数据
case "user":
// data = 获取用户相关数据
case "visitor":
// data = 获取游客相关数据
}
return data;
}
// 做一个小转换
const getDataProcessor = {
admin() {
// return 获取管理员数据
},
user() {
// return 获取用户相关数据
},
visitor() {
// return 获取游客相关数据
},
};
function getData(account) {
return getDataProcessor[account]();
}
// 当然,实际上Processor的映射过程会复杂很多
状态模式
// 状态模式和策略模式相似,但状态模式强调了类的概念
// 即把处理过程放在处理对象本身中
class CoffeeMaker {
//
constructor() {
this.state = "init";
this.stock = {
coffee: 500,
milk: 200,
};
}
//
changeState(state) {
this.state = state;
this.changeStateProcessor[state]();
}
//
changeStateProcessor = {
that: this,
coffee() {
console.log("咖啡机现在的咖啡存储量是:", this.that.stock.coffee);
console.log("我只吐黑咖啡");
},
latte() {
this.coffee();
console.log("咖啡机现在的咖啡存储量是:", this.that.stock.milk);
console.log("加点奶");
},
vanillaLatte() {
this.latte();
console.log("再加香草糖浆");
},
mocha() {
this.latte();
console.log("再加巧克力");
},
};
}
// 测试一下
const coffeeMaker = new CoffeeMaker();
coffeeMaker.changeState("latte");
观察者模式
// 观察者订阅发布者,发布者能群通知观察者们
class Publisher {
constructor() {
this.observers = [];
}
addObserver(observer) {
this.observers.push(observer);
}
removeObserver(observer) {
this.observers.forEach((item, index) => {
if (item === observer) this.observers.splice(index, 1);
});
}
notifyObersers() {
this.observers.forEach((item) => {
item.update(this);
});
}
}
class Observer {
update(publisher) {
// 更新操作
}
}
// 在上面两个类中扩展出具体的类
class PMPublisher extends Publisher {
constructor() {
super();
this.state = null;
this.observers = [];
}
getState() {
return this.state;
}
setState(state) {
this.state = state;
this.notifyObersers();
}
}
class PMObserver extends Observer {
constructor(id) {
super();
this.id = id;
this.state = null;
}
update(publisher) {
this.state = publisher.getState();
this.work();
}
work() {
console.log(`员工 ${this.id} 接收状态 ${this.state},开始干活啦~`);
}
}
// 测试一下
const xiaoqiang = new PMPublisher(),
xiaoting = new PMObserver("#01"),
xiaoyong = new PMObserver("#02"),
xiaolong = new PMObserver("#03");
xiaoqiang.addObserver(xiaoting);
xiaoqiang.addObserver(xiaoyong);
xiaoqiang.addObserver(xiaolong);
xiaoqiang.setState("开会");
发布——订阅模式
// 观察者模式中,发布者直接接触订阅者
// 发布——订阅模式中,发布者和订阅者通过中间平台间接接触
// 譬如下面的事件发布/订阅器
class EventEmitter {
// 构造函数
constructor() {
this.handlers = {};
}
// 注册事件监听器
on(event, callback) {
if (!this.handlers[event]) this.handlers[event] = [];
this.handlers[event].push(callback);
}
// 移除某个事件回调队列里的指定回调函数
off(event, callback) {
const callbacks = this.handlers[event],
index = callbacks.indexOf(callback);
if (index !== -1) {
callbacks.splice(index, 1);
}
}
// 触发目标事件
emit(event, ...params) {
if (this.handlers[event]) {
this.handlers[event].forEach((callback) => {
callback(...params);
});
}
}
// 为事件注册单次监听器
once(event, callback) {
// 对回调函数进行包装,使其执行完毕自动被移除
const wrapper = (...params) => {
callback(...params);
this.off(event, wrapper);
};
this.on(event, wrapper);
}
}
迭代器模式
// 迭代器模式旨在为不同类型的集合提供遍历接口
// ES6的生成器(迭代器生成函数)能很方便地生成迭代器
function* iteratorGenerator() {
yield "#1";
yield "#2";
yield "#3";
}
const iterator = iteratorGenerator();
iterator.next(); // '#1'
iterator.next(); // '#2'
iterator.next(); // '#3'
// 自己手写一个生成器
function iteratorGenerator(list) {
let index = 0,
length = list.length;
return {
next() {
let done = index >= length,
value = !done ? list[index++] : undefined;
return { done, value };
},
};
}
const iterator = iteratorGenerator(["#1", "#2", "#3"]);
iterator.next(); // '#1'
iterator.next(); // '#2'
iterator.next(); // '#3'
// good!