行为型设计模式
- 用于不同 对象间 职责划分或算法抽象
- 代码:https://github.com/baixc1/csdn/tree/master/DesignPatterns/Behavior
模版方法模式
核心
- 定义:父类定义一组算法骨架,子类在复用父类的同时,扩展功能
- 应用:公共组件的封装,基础类组件封装(定制化组件,提供多个类型组件的统一修改入口)
- 重点:子类型,共享父类型组件的样式及功能,利于功能统一,代码维护和扩展。(方法重用、共享)
示例一:提示框
- 需求:封装一个基本提示框,在其基础上扩展不同类型的子类提示框(利于样式统一的维护)
- js
// 模版类 基础提示框(面板,提示内容,关闭按钮,确认按钮)
var g = (tag) => document.createElement(tag);
var Alert = function (data) {
if (!data) return;
// 默认数据
data = {
confirm: "确认",
success() {},
fail() {},
...data,
};
this.content = data.content;
// 标签
this.panel = g("div");
this.contentNode = g("p");
this.contentNode.innerHTML = this.content;
this.confirmBtn = g("span");
this.confirmBtn.innerHTML = data.confirm;
this.closeBtn = g("b");
// 样式
this.panel.className = "alert";
this.confirmBtn.className = "a-confirm";
this.closeBtn.className = "a-close";
// 方法
this.success = data.success;
this.fail = data.fail;
};
// 原型方法
Alert.prototype = {
init() {
// 生成提示框
[this.closeBtn, this.contentNode, this.confirmBtn].forEach((el) => {
this.panel.appendChild(el);
});
document.body.appendChild(this.panel);
this.bindEvent();
this.show();
},
// 绑定事件
bindEvent() {
this.closeBtn.onclick = () => {
this.fail();
this.hide();
};
this.confirmBtn.onclick = () => {
this.success();
this.hide();
};
},
hide() {
this.panel.style.display = "none";
},
show() {
this.panel.style.display = "block";
},
};
// 提示框(右侧按钮)
var RightAlert = function (data) {
Alert.call(this, data);
this.confirmBtn.className += " right";
};
RightAlert.prototype = Object.create(Alert.prototype);
RightAlert.prototype.constructor = RightAlert;
console.log(RightAlert.prototype);
// 标题提示框
var TitleAlert = function (data) {
Alert.call(this, data);
this.title = data.title;
this.titleNode = g("h3");
this.titleNode.innerHTML = this.title;
};
TitleAlert.prototype = new Alert();
TitleAlert.prototype.init = function () {
this.panel.insertBefore(this.titleNode, this.panel.firstChild);
Alert.prototype.init.call(this);
};
console.log(TitleAlert.prototype);
// 标题提示框+取消按钮
var CancelAlert = function (data) {
TitleAlert.call(this, data);
this.cancel = data.cancel;
this.cancelBtn = g("span");
this.cancelBtn.className = "cancel";
this.cancelBtn.innerHTML = this.cancel || "取消";
};
// 继承 Alert 原型
CancelAlert.prototype = new Alert();
CancelAlert.prototype.init = function () {
// 继承 TitleAlert原型 的 init
TitleAlert.prototype.init.call(this);
this.panel.appendChild(this.cancelBtn);
};
CancelAlert.prototype.bindEvent = function () {
// 继承事件
TitleAlert.prototype.bindEvent.call(this);
this.cancelBtn.onclick = () => {
this.fail();
this.hide();
};
};
- 调用
new Alert({
content: "我是基础弹窗",
}).init();
new RightAlert({
content: "我是右侧按钮弹窗",
}).init();
new TitleAlert({
content: "我是标题提示框",
title: "我是标题",
}).init();
new CancelAlert({
content: "标题提示框+取消按钮",
title: "我是标题",
cancel: "取消按钮",
fail() {
console.log("fail..");
},
}).init();
- 效果图
示例二:多类导航
- 需求:创建一个基础导航,在其基础上,扩展出消息导航和链接导航
- js
// 格式化字符串
function formatString(str, data) {
// 全局匹配 {#xxx#} -> xxx(数字,字母,下划线)
return str.replace(/\{#(\w+)#\}/g, function (match, key) {
console.log(match);
return (data && data[key]) || "";
});
}
// 基础导航
var Nav = function (data) {
// 模板
this.item = '<a href="{#href#}" title="{#title#}">{#name#}</a>';
this.html = "";
for (const v of data) {
this.html += formatString(this.item, v);
}
return this.html;
};
// 信息导航
var NumNav = function (data) {
// 模板
var tpl = "<b>{#num#}</b>";
for (let i = 0; i < data.length; i++) {
data[i].name += formatString(tpl, data[i]);
}
return Nav.call(this, data);
};
// 网址导航
var LinkNav = function (data) {
var tpl = "<span>{#link#}</span>";
for (let i = 0; i < data.length; i++) {
data[i].name += formatString(tpl, data[i]);
}
return Nav.call(this, data);
};
- 调用
<script>
const list = [
{
href: "http://www.baidu.com",
title: "百度一下",
name: "百度",
},
{
href: "http://www.baidu.com",
title: "百度一下1",
name: "百度1",
},
{
href: "http://www.baidu.com",
title: "百度一下2",
name: "百度2",
},
];
document.getElementById("main").innerHTML = Nav(list);
document.getElementById("main1").innerHTML = NumNav(
list.map((item) => ({
...item,
num: Math.floor(Math.random() * 5) + 5,
}))
);
document.getElementById("main2").innerHTML = LinkNav(
list.map((item, index) => ({
...item,
link: `(网址:${new Array(index + 3).fill("x").join("")})`,
}))
);
</script>
- 效果图
观察者模式
核心
- 定义:又叫发布 - 订阅者模式,或消息机制。定义了一种 依赖关系,解耦对象间的关系(主体对象和观察者)
- 应用:前端中的事件机制(注册和解绑由开发者实现,由浏览器等发布事件),功能模块间的通信(解耦)
- 核心:解决代码的耦合问题,实现模块间的通信
观察者对象实现
- 观察者对象实现(容器对象):负责注册、注销、发布功能的实现
// 观察者对象(绑定,解绑,发布)
var Observer = (function () {
// 消息容器
var _message = {};
return {
// 绑定,type - 消息类型,fn - 回调函数
on(type, fn) {
if (!_message[type]) _message[type] = [];
_message[type].push(fn);
return this;
},
// 解绑
off(type, fn) {
if (!_message[type] instanceof Array) return;
var len = _message[type].length,
i = len - 1;
// 从后往前遍历,优化删除元素的性能
for (; i >= 0; i--) {
if (_message[type][i] === fn) {
_message[type].splice(i, 1);
}
}
return this;
},
// 发布
emit(type, args = {}) {
if (!_message[type]) return;
// 消息信息
var events = {
type,
args,
};
var i = 0,
len = _message[type].length;
for (; i < len; i++) {
_message[type][i].call(this, events); // this 绑定off的调用者
}
return this;
},
};
})();
if (module) {
module.exports = Observer;
}
- 基本功能测试
const Observer = require("./common");
// 绑定和删除函数时,地址相同
const f1 = (events) => console.log("触发f1,参数:", events);
const f2 = (events) => console.log("触发f2,参数:", events);
const f3 = function () {
console.log("this === Observer:", this === Observer);
};
Observer.on("click", f1);
Observer.on("click", f2);
Observer.on("click", f3);
Observer.emit("click", { a: 1, b: 2 });
Observer.off("click", f1);
Observer.off("click", f3);
Observer.emit("click", { a: 3, b: 2 });
- 效果图
示例一:新闻评论功能
-
需求:功能分三块:用户信息(展示消息数),消息列表(展示和删除消息),提交消息(新增消息)
-
说明
- 用户信息模块,信息接收者
- 消息列表,接受者和发送者
- 提交消息,发送者
-
js
// 新闻评论功能 模块通信(用户消息信息+消息列表+提交表单)
// 根据ID获取dom
function $(id) {
return document.getElementById(id);
}
// 创建dom
function g(tag) {
return document.createElement(tag);
}
// 用户信息模块(显示消息)
(function () {
let num = 0;
function change(e) {
num += e.args.num;
$("msg_num").innerHTML = num;
}
// 注册 添加/删除事件
Observer.on("add", change).on("remove", change);
})();
// 消息列表模块(显示消息+添加消息)
(function () {
function add(e) {
var text = e.args.text,
ul = $("msg"),
li = g("li"),
span = g("span");
li.innerHTML = text;
span.innerHTML = "删除";
span.onclick = () => {
ul.removeChild(li);
// 发布删除事件
Observer.emit("remove", { num: -1 });
};
li.appendChild(span);
ul.appendChild(li);
}
// 注册添加事件
Observer.on("add", add);
})();
// 提交表单模块
(function () {
$("submit").onclick = function () {
var text = $("input");
if (!text.value) return;
// 发布添加事件
Observer.emit("add", {
text: text.value,
num: 1,
});
text.value = "";
};
})();
- 调用
<script src="./common.js"></script>
<body>
<div>
<p>用户信息</p>
<span>消息数:</span>
<span id="msg_num"></span>
</div>
<div>
消息列表
<ul id="msg"></ul>
</div>
<div>
<p>提交消息</p>
<textarea id="input"></textarea>
<button id="submit">提交</button>
</div>
<script src="./index2.js"></script>
</body>
- 效果图
示例二:对象间解耦
- 需求:课堂老师提问+学生回答
- 实现
// 对象间解耦
// 示例:课堂老师提问+学生回答
const Observer = require("./common");
// 学生类
var Student = function (res) {
// 回答结果
this.res = res;
// 回答问题动作
this.say = () => {
console.log(this.res);
};
};
// 回答问题方法
Student.prototype.answer = function (question) {
// 注册问题事件
Observer.on(question, this.say);
};
// 学生睡觉,不能回答问题
Student.prototype.sleep = function (question) {
console.log(this.res + " " + question + " 已注销");
// 解绑问题事件
Observer.off(question, this.say);
};
// 教师类
var Teacher = function () {};
Teacher.prototype.ask = function (question) {
console.log("老师问问题:", question);
// 触发问题事件
Observer.emit(question);
};
var s1 = new Student("学生1回答");
var s2 = new Student("学生2回答");
var s3 = new Student("学生3回答");
// 学生注册事件
s1.answer("老师的问题1");
s1.answer("老师的问题2");
s2.answer("老师的问题1");
s3.answer("老师的问题1");
s3.answer("老师的问题2");
// 学生睡觉(在问题1)
s3.sleep("老师的问题1");
var t = new Teacher();
// 老师问问题
t.ask("老师的问题1");
t.ask("老师的问题2");
状态模式
核心
- 定义:对象的行为,由其内部状态决定(简化分支逻辑,利于扩展)
- 核心:分支逻辑较多时,利用 js对象 可以根据 key 直接查找到 value 的特性,用对象的方法替代分支逻辑
- 应用:动态组件,类型常量定义等
示例一:投票活动
- 需求:给投票的结果,设置不同的处理函数
- 核心:利用闭包,生成内部变量,控制访问
// 投票结果状态对象
var ResultState = (function () {
var States = {
state0() {
console.log("第一种情况");
},
state1() {
console.log("第二种情况");
},
state2() {
console.log("第三种情况");
},
state3() {
console.log("第四种情况");
},
};
function show(res) {
States[`state${res}`] && States[`state${res}`]();
}
// 闭包
return {
show,
};
})();
ResultState.show(1);
示例二:超级玛丽
- 需求:超级玛丽游戏,人物有多种状态:跳跃、移动、射击、蹲下…,状态可以搭配组合。对其动作行为进行封装。
- 核心:内部变量控制人物的当前状态,提供访问和修改状态的接口
// 超级玛丽状态类
var MarryState = function () {
// 状态(私有变量)
var _state = {};
// 动作类型
var types = {
jump() {
console.log("jump"); // 跳跃
},
move() {
console.log("move"); // 移动
},
shoot() {
console.log("shoot"); // 射击
},
quat() {
console.log("quat"); // 蹲下
},
};
// 动作类
var Action = {
changeState() {
var arg = arguments;
_state = {};
for (var i = 0; i < arg.length; i++) {
_state[arg[i]] = true; // 添加动作
}
return this;
},
goes() {
console.log("触发一次动作");
for (var key in _state) {
types[key] && types[key]();
}
return this;
},
};
return {
change: Action.changeState,
goes: Action.goes,
};
};
MarryState()
.change("quat", "shoot", "move") // 蹲下-射击-移动
.goes()
.goes()
.change("move", "jump") // 移动-跳跃
.goes();
console.log("--------------------------------");
// 实例化后使用(不影响原对象)
var marry = new MarryState();
marry
.change("quat", "shoot", "move") // 蹲下-射击-移动
.goes()
.goes()
.change("move", "jump") // 移动-跳跃
.goes();
- 效果图
策略模式
核心
- 定义:封装一组算法,用于特定的功能
- 应用:表单的验证方法,业务功能相似的工具方法封装
- 重点:使业务和功能解耦,利于封装和维护
示例一:商品促销
- 需求:商品促销,满减活动,打折活动
// 价格策略对象
var PriceStrategy = (function () {
// 内部算法对象
var strategy = {
// 100 返 30
return30(price) {
return +price + parseInt(price / 100) * 30;
},
// 100 返 50
return50(price) {
return +price + parseInt(price / 100) * 50;
},
// 9折
percent90(price) {
return price * 0.9;
},
// 8折
percent80(price) {
return price * 0.8;
},
};
// 策略算法接口
return function (type, price) {
return strategy[type] && strategy[type](price);
};
})();
console.log(PriceStrategy("percent90", "400.9"));
示例二:表单验证
- 需求:验证不同的表单元素,是否符合要求(必填、手机号、邮箱、网址…)
// 表单验证
var InputStrategy = (function () {
var strategy = {
notNull(value) {
return /\s+/.test(value) ? "请输入内容" : "";
},
number(value) {
return /^[0-9]+(\.[0-9]+)?$/.test(value) ? "" : "请输入正确的数字";
},
phone(value) {
return /^\d{3}-\d{8}$|^\d{4}-\d{7}$/.test(value)
? ""
: "请输入正确的电话号码";
},
};
return {
check(type, value) {
value = value.replace(/^\s+|\s+$/g, "");
return strategy[type] ? strategy[type](value) : "没该类型方法";
},
addStrategy(type, fn) {
strategy[type] = fn;
},
};
})();
// 返回验证信息,空为通过
console.log(InputStrategy.check("phone", " 333-88888888 ")); // 输出 空
职责链模式
核心
- 定义:解决请求的发送者和请求的接受者之间的耦合
- 简化拆分 发送 - 接收 的流程(类似于中间件)
- 应用:业务需求的渐进式开发(向后兼容)。类似于渐进式开发,业务由易 -> 难的递增性开发(react全家桶,中间件…)(利于单元测试)
示例
- 需求:表单元素的异步校验(请求后端校验)
- 功能拆分
- 请求模块
- 数据处理模块
- 创建组件模块
- 测试模块
- 服务端:使用 express 做 web 服务器
- 监听 /getData的 get 请求,服务端页面渲染
- 校验不同的表单元素(组合校验)
var express = require("express");
var app = express();
app.use(express.static("./"));
app.get("/getData", function (req, res) {
const { key, value } = req.query;
if (key === "name") {
if (value.length < 2) {
res.send(["请输入至少2个"]);
return;
}
}
if (key === "num") {
var errs = [];
if (!/^\d+$/.test(value)) {
errs.push("请输入数字");
}
if (value.length < 4) {
errs.push("请输入至少四位");
}
if (errs.length) return res.send(errs);
}
res.send([]);
});
app.listen(3000);
- js
// ajax简单封装(未兼容IE)
var sendData = function (data, dealType, dom) {
var xhr = new XMLHttpRequest();
xhr.responseType = "json";
var url = "getData?";
xhr.onload = function (e) {
console.log(xhr);
const { status } = xhr;
if ((status >= 200 && status < 300) || status === "304") {
dealData(xhr.response, dealType, dom);
} else {
// 失败
}
};
for (var key in data) {
url += `&${key}=${data[key]}`;
}
xhr.open("get", url, true);
xhr.send(null);
};
var dealData = function (data, dealType, dom) {
var dataType = Object.prototype.toString.call(data);
switch (dealType) {
case "sug":
// 数组
if (dataType === "[object Array]") {
return createSug(data, dom);
}
// 对象转数组
if (dataType === "[object Object]") {
var list = [];
for (var key in data) {
list.push(data[key]);
}
return createSug(list, dom);
}
// 其他数据转数组
return createSug([data], dom);
case "validate":
return createValidate(data, dom);
}
};
// 提示组件
var createSug = function (data, dom) {
dom.parentNode.getElementsByTagName("ul")[0].innerHTML = data.reduce(
(prev, next) => {
return prev + `<li>${next}</li>`;
},
""
);
};
// 校验组件(模拟)
var createValidate = function (data, dom) {
dom.parentNode.getElementsByTagName("span")[0].innerHTML = data;
};
// 测试
var input = document.getElementsByTagName("input");
[...input].forEach((el) => {
const { dealtype: dealType, trigger, key } = el.dataset;
// 绑定事件
el[`on${trigger}`] = function (e) {
setTimeout(() => {
sendData({ value: el.value, key }, dealType, this);
}, 0);
};
});
- 调用
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div>
<div>
<label>名称(验证功能输入框)</label>
<input
placeholder="名称(验证功能输入框)"
data-dealType="validate"
data-trigger="change"
data-key="name"
/>
<ul></ul>
<span></span>
</div>
<div>
<label>数字(提示功能输入框)</label>
<input
placeholder="数字(提示功能输入框)"
data-dealType="sug"
data-trigger="input"
data-key="num"
/>
<ul></ul>
<span></span>
</div>
</div>
<script src="./index.js"></script>
</body>
</html>