1: 迭代器模式
迭代器模式: 提供一种方法顺序访问一个聚合对象中的各个元素,使用者并不需要关心该方法的内部表示. 作用
- 为遍历不同集合提供统一接口
- 保护原集合但又提供外部访问内部元素的方式
实际案例
前端开发中会接触到各种数组或者类数组对象,在 jQuery 中可以通过$.each 等接口遍历各种列表,当然 JS 也内置了多种遍历数组的方法如forEach
、reduce
等。
对于数组的循环大家都轻车熟路了,在实际开发中,也可以通过循环来优化代码。
一个常见的开发场景是:通过 ua 判断当前页面的运行平台,方便执行不同的业务逻辑,最基本的写法当然是if...else
一把梭
const PAGE_TYPE = {
app: "app", // app
wx: "wx", // 微信
tiktok: "tiktok", // 抖音
bili: "bili", // B站
kwai: "kwai", // 快手
};
function getPageType() {
const ua = navigator.userAgent;
let pageType;
// 移动端、桌面端微信浏览器
if (/xxx_app/i.test(ua)) {
pageType = app;
} else if (/MicroMessenger/i.test(ua)) {
pageType = wx;
} else if (/aweme/i.test(ua)) {
pageType = tiktok;
} else if (/BiliApp/i.test(ua)) {
pageType = bili;
} else if (/Kwai/i.test(ua)) {
pageType = kwai;
} else {
// ...
}
return pageType;
}
复制代码
判断的逻辑很简单,遍历当前需要判断的平台列表,并返回第一个匹配的平台类型,可以看见,当我们需要新增一种平台的判断时,要做修改两个地方
- 修改
PAGE_TYPE
的枚举值,增加新的平台名称 - 修改
getPageType
中的实现,增加一个else if
分支
同理,移除某种平台的判断,也需要修改这两个地方。
参考策略模式的思路,我们可以减少分支判断的出现,将每个平台的判断拆分成单独的策略
function isApp(ua) {
return /xxx_app/i.test(ua);
}
function isWx(ua) {
return /MicroMessenger/i.test(ua);
}
function isTiktok(ua) {
return /aweme/i.test(ua);
}
function isBili(ua) {
return /BiliApp/i.test(ua);
}
function isKwai(ua) {
return /Kwai/i.test(ua);
}
每个策略都定义了相同的函数签名:接收 ua 字符串并返回布尔值,为 true 表示匹配。然后重新实现getPageType
let platformList = [
{ name: "app", validator: isApp },
{ name: "wx", validator: isWx },
{ name: "tiktok", validator: isTiktok },
{ name: "bili", validator: isBili },
{ name: "kwai", validator: isKwai },
];
function getPageType() {
// 每个平台的名称与检测方法
const ua = navigator.userAgent;
// 遍历
for (let { name, validator } in platformList) {
if (validator(ua)) {
return name;
}
}
}
这样,整个getPageType
方法就变得非常简洁:按顺序遍历platformList
,返回第一个匹配上的平台名称作为 pageType。
这样即使后面需要增加或移除平台判断,需要修改的仅仅也只是platformList
这个地方而已。比如将头条新闻 APP 也算作 tiktok 的话,只需要修改isTiktok
function isTiktok(ua) {
return /aweme|NewsArticle/i.test(ua);
}
类似的条件判断还有很多,比如为了兼容老浏览器找到一个合适的 XHR 版本对象、根据 app 版本号使用不同的客户端接口等。只要代码修改的范围足够小,产生 bug 的几率就越小。
在上面的例子中,我们更像是在使用策略模式或者责任链模式,却在不知不觉中使用到了迭代器模式。大部分现代语言都内部了迭代器,我们没必要刻意去实现prev
、next
、isDone
等接口,而应该学会灵活使用迭代器来简化代码逻辑。
2:单例模式
单例模式: 保证一个类只有一个实例, 一般先判断实例是否存在,如果存在直接返回, 不存在则先创建再返回,这样就可以保证一个类只有一个实例对象.
class MessageBox {
show() {
console.log("show");
}
hide() {}
static getInstance() {
if (!MessageBox.instance) {
MessageBox.instance = new MessageBox();
}
return MessageBox.instance;
}
}
let box3 = MessageBox.getInstance();
let box4 = MessageBox.getInstance();
console.log(box3 === box4); // true
3策略模式
主要作用是:将层级相同的逻辑封装成可以组合和替换的策略方法,减少 if...else
代码,方便扩展后续功能。
说到前端代码中的 if...else 代码,最常见的恐怕就是表单校验了
function onFormSubmit(params) {
if (!params.nickname) {
return showError("请填写昵称");
}
if (params.nickname.length > 6) {
return showError("昵称最多6位字符");
}
if (!/^1\d{10}$/.test(params.phone))
return showError("请填写正确的手机号");
}
// ...
sendSubmit(params)
}
复制代码
关于 if..else 代码的罪过想必大家都比较熟悉了,这种写法还有一些额外的问题
- 将所有字段的校验规则都堆叠在一起,如果想查看某个字段的校验规则,则需要将所有的判断都看一遍(避免某个同事将同一个字段的两种判断放在了不同的位置)
- 在遇见错误时,直接通过 return 跳过了后面的判断;如果产品希望直接展示每个字段的错误,则改动的工作量可不谓不少。
不过目前在antd
、ELementUI
等框架盛行的年代,在 Form 组件中已经很少看见这种代码了,这要归功于async-validator。
下面我们来实现一个建议的validator
class Schema {
constructor(descriptor) {
this.descriptor = descriptor;
}
validate(data) {
return new Promise((resolve, reject) => {
let keys = Object.keys(data);
let errors = [];
for (let key of keys) {
const config = this.descriptor[key];
if (!config) continue;
const { validator } = config;
try {
validator(data[key]);
} catch (e) {
errors.push(e.toString());
}
}
if (errors.length) {
reject(errors);
} else {
resolve();
}
});
}
}
复制代码
然后声明每个字段的校验规则,
// 首先声明每个字段的校验规则
const descriptor = {
nickname: {
validator(val) {
if (!val) {
throw "请填写昵称";
}
if (val.length < 6) {
throw "昵称最多6位字符";
}
},
},
phone: {
validator(val) {
if (!val) {
throw "请填写电话号码";
}
if (!/^1\d{10}$/.test(val)) {
throw "请填写正确的手机号";
}
},
},
};
复制代码
最后校验数据源
// 开始校验
const validator = new Schema(descriptor);
const params = { nickname: "", phone: "123000" };
validator
.validate(params)
.then(() => {
console.log("success");
})
.catch((e) => {
console.log(e);
});
复制代码
可以看见,Schema
主要暴露了构造参数和validate
两个接口,是一个通用的工具类,而params
是表单提交的数据源,因此主要的校验逻辑实际上是在descriptor
中声明的。
在上面的实现中,我们按照字段的维度,为每个字段实现了一个validator
方法,用于处理校验该字段需要的逻辑。
实际上我们可以拆分出一些更通用的规则,比如required
(必填)、len
(长度)、min/max
(最值)等,这样,当多个字段存在一些类似的校验逻辑时,可以尽可能地复用。
修改一下 descriptor,将每一个字段的校验规则类型修改为列表,列表中每个元素的 key 表示这条规则的名称,validator
作为自定义规则
const descriptor = {
nickname: [
{ key: "required", message: "请填写昵称" },
{ key: "max", params: 6, message: "昵称最多6位字符" },
],
phone: [
{ key: "required", message: "请填写电话号码" },
{
key: "validator",
params(val) {
return !/^1\d{10}$/.test(val);
},
message: "请填写正确的电话号码",
},
],
};
复制代码
然后修改Schema
的实现,增加handleRule
方法
class Schema {
constructor(descriptor) {
this.descriptor = descriptor;
}
handleRule(val, rule) {
const { key, params, message } = rule;
let ruleMap = {
required() {
return !val;
},
max() {
return val > params;
},
validator() {
return params(val);
},
};
let handler = ruleMap[key];
if (handler && handler()) {
throw message;
}
}
validate(data) {
return new Promise((resolve, reject) => {
let keys = Object.keys(data);
let errors = [];
for (let key of keys) {
const ruleList = this.descriptor[key];
if (!Array.isArray(ruleList) || !ruleList.length) continue;
const val = data[key];
for (let rule of ruleList) {
try {
this.handleRule(val, rule);
} catch (e) {
errors.push(e.toString());
}
}
}
if (errors.length) {
reject(errors);
} else {
resolve();
}
});
}
}
复制代码
这样,就可以将常见的校验规则都放在ruleMap
中,并暴露给使用者自己组合各种校验规则,比之前各种不可复用的 if..else 判断会更容易维护和迭代。
4:代理模式
代理模式是为一个对象提供一个代用品或占位符,以便控制对它的访问