这是我参与「第四届青训营 」笔记创作活动的第3天
什么是设计模式?
** 设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。**简单来讲,设计模式就是一套工程的模板,遇到什么问题的时候该用怎样的固定或近似不变的方式来解决问题。
设计模式的分类(23种):
- 创建型
1、简单工厂模式
2、工厂方法模式
3、抽象工厂模式
4、建造者模式
5、原型模式
6、单例模式
- 结构型
7、外观模式
8、适配器模式
9、代理模式
10、装饰者模式
11、桥接模式
12、组合模式
13、享元模式
- 行为型
14、模板方法模式
15、观察者模式
16、状态模式
17、策略模式
18、职责链模式
19、命令模式
20、访问者模式
21、中介者模式
22、备忘录模式
23、迭代器模式
浏览器中的设计模式:单例模式与发布订阅模式
-
单例模式:一个类只有一个实例,只有全局唯一的访问对象,也就是说第一次采用类创建的实例与第二次采用类创建的实例完全相同,在实现实例的过程中可以用静态属性来实现
优点:1.只存在一个实例对象,节约系统资源,需要频繁的创建销毁单例的时候提高系统的性能
2.防止其他对象对自己实例化,确保每个对象都访问同一个实例 3.具有伸缩性,灵活度很高缺点:1.不适用于变化的对象
2.单例的扩展有很大的问题 3.对象状态有可能会丢失应用场景:缓存,全局状态管理(document,window都是很好的体现)
实现:
class Person{
static instance;
constructor(name){
if(Person.instance){
return Person.instance;
}else{
Person.instance = this;
}
this.name = name;
}
}
//普通实现
function Single(name) {
this.name = name;
}
// 静态方法
Single.getInstance = (function() {
let instance; // 使用闭包实现单例模式
return function(name) {
if (!instance) { // 注意判断条件
instance = new Single(name);
}
return instance;
}
})();
// 测试
let a = Single.getInstance('ins1');
let b = Single.getInstance('ins2');
console.log(a === b); // true
//闭包实现
- 发布订阅模式:对象之间一对多的依赖关系,一个对象的状态改变时,所有依赖它的对象状态都会发生改变,实际上发布-订阅模式是基于消息联通的,必须在订阅方和发布方是同一个消息时才会有执行结果。订阅者只关注自己订阅的消息,每个订阅者同时可以订阅多个发布者对象。每个发布者在发布消息时不用关心此消息是否有订阅者,当发布者发布了被订阅者订阅的消息时,那么订阅者就根据消息详情做出对应的处理。
应用场景:最经典的应用场景类似于B站的关注up主或油管的关注博主类似,订阅了某个喜欢的博主,被订阅对象根据订阅对象作出反应,发布内容更新或者自动私信的功能
实现:
class EventEmitter {
constructor() {
this.listener = {}; // 存储事件监听器和回调函数
}
// 注册事件的回调函数
on(event, callback) {
if (!this.listener[event]) {
this.listener[event] = []; // 初始化一个监听函数队列
}
this.listener[event].push(callback);
}
// 触发注册事件的回调函数执行,注意参数
emit(event, ...args) {
if (this.listener[event]) {
this.listener[event].forEach((cb) => {
cb(...args);
})
}
}
// 删除一个回调函数
off(event, callback) {
if (this.listener[event]) {
let cbArray = this.listener[event]; // 取出对应的监听函数队列
let pos = cbArray.indexOf(callback);
if (pos !== -1) {
this.listener[event].splice(pos, 1); // 从队列中删除该函数
}
if (this.listener[event].length === 0) {
delete this.listener[event]; // 移除该事件
}
}
}
// 注册事件的回调函数,只执行一次
once(event, callback) {
// 对回调函数进行包装,使其执行完毕自动被移除
const wrapper = (...args) => {
callback.apply(this, args);
this.off(event, callback);
}
this.on(event, wrapper);
}
// 删除某个事件的所有监听函数
offAll(eventName) {
if (this.listeners[eventName]) {
delete this.listeners[eventName];
}
}
}
/* 测试 */
let ee = new EventEmitter();
ee.on('eat', function() { console.log('吃饭'); });
ee.emit('eat');
\
JavaScript里的设计模式
- 原型模式:复制已有的对象来创建新的对象,在实际编程中类似于我们new一个新的实例对象,最经典的应用就是创建对象的过程,本质上,这种模式是实现了一个原型接口用于创建新的对象,和单例模式相反
实现方法:object.create(原型模式的天然实现),得到与构造函数相对应的类型的实例,共享数据与方法,相对于设计模式,原型模式更接近与一种编程范式
实现:
function Person(){
}
//给原型添加自定义属性和方法
Person.prototype.name = '张三';
Person.prototype.sayName = function(){
console.log(this.name);
}
var p1 = new Person();
//给p1添加age属性
p1.age = 18;
console.log(p1.name);//张三
console.log(p1.age);//18
p1.sayName();//张三
var p2 = new Person();
console.log(p2.name);//张三
console.log(p2.age);//undefined
p2.sayName();//张三
- 代理模式:为一个对象提供一个代用品或占位符,以便控制它的访问。简单的来说,就是找一个对象“替身”来代替另一个对象,生活中的例子比如说明星不方便在公共场合出现购买物品于是请自己的经纪人代替他去完成
应用场景:
- 虚拟代理(图片预加载,合并HTTP请求)
- 缓存代理(缓存异步请求数据,缓存复杂的运算结果)
- ES6中的proxy(实现对象的私有属性,实现表单验证)
优点:
- 可以拦截或者监听外部对本体对象的访问,在安全性上具有较好的应用
- 进行复杂的运算存储管理,提高性能
- 可添加拓展功能
缺点:
- 代理对象的创建会增加内存开销
**ES6中的代理模式实现
ES6 的 Proxy语法:let proxyObj = new Proxy(target, handler);
- target: 本体,要代理的对象
- handler: 自定义操作方法集合
- proxyObj: 返回的代理对象,拥有本体的方法,不过会被 handler 预处理
实现:
// ES6的Proxy
let Person = {
name: 'dty'
};
const ProxyPerson = new Proxy(Person, {
get(target, key, value) {
if (key != 'age') {
return target[key];
} else {
return '保密'
}
},
set(target, key, value) {
if (key === 'rate') {
target[key] = value === 'A' ? '推荐' : '待提高'
}
}
})
console.log(ProxyPerson.name); // 'dty'
console.log(ProxyPerson.age); // '保密'
ProxyPerson.rate = 'A';
console.log(ProxyPerson.rate); // '推荐'
ProxyPerson.rate = 'B';
console.log(ProxyPerson.rate); // '待提高'
- 迭代器模式:提供一种方法顺序一个聚合对象中各个元素,而又不暴露该对象内部表示。和编程设计思想里的封装思想有异曲同工之处,简单来说,对js里数据结构的访问类似于如此其应用场景包括对列表,DOM树等进行通用操作接口
实现:
// 统一遍历接口实现
var each = function(arr, callBack) {
for (let i = 0, len = arr.length; i < len; i++) {
// 将值,索引返回给回调函数callBack处理
if (callBack(i, arr[i]) === false) {
break; // 中止迭代器,跳出循环
}
}
}
// 外部调用
each([1, 2, 3, 4, 5], function(index, value) {
if (value > 3) {
return false; // 返回false中止each
}
console.log([index, value]);
})
// 输出:[0, 1] [1, 2] [2, 3]
\
前端框架中的设计模式
- 组合模式:类似的api用于多个对象组合使用。或者用户单独使用,应用场景包括DOM,前端组件文件目录等。
优点:客户端一直出来不担心处理为单个对象还是组合对象
缺点:设计复杂,缺少普遍性
实现简单的文件系统:
abstract class FileComponent {
isFile: boolean;
fileName: string;
constructor(fileName: string, isFile: boolean) {
this.isFile = isFile;
this.fileName = fileName;
}
abstract add(file: FileLeaf | Folder): FileComponent;
abstract getFileCount(): number;
}
// 组合节点
class Folder extends FileComponent {
// 这里是比较核心的点,树结构用来
list: FileLeaf[] = [];
constructor(fileName: string, isFile: boolean) {
super(fileName, isFile);
}
add(file: FileLeaf): FileComponent {
const result = this.list.some((item) => item.fileName === file.fileName);
if (result) return this;
this.list.push(file);
return this;
}
getFileCount() {
const fileCount = this.list.reduce((accumulator, item) => {
if (item.isFile) return accumulator + 1;
return accumulator;
}, 0);
return fileCount;
}
}
// 叶子节点
class FileLeaf extends FileComponent {
constructor(fileName: string, isFile: boolean) {
super(fileName, isFile);
}
add(): FileLeaf {
throw new Error("文件不可执行该操作。");
}
getFileCount(): number {
return 1;
}
}
// 创建一个根目录
const root = new Folder('root', false);
// 创建两个目录
const folder = new Folder('folder', false);
// 创建三个文件
const file1 = new FileLeaf('file1', true);
const file2 = new FileLeaf('file2', true);
const file3 = new FileLeaf('file3', true);
folder.add(file3);
root.add(file1)
.add(file2)
.add(folder);
console.log(root.getFileCount()); // 2
console.log(folder.getFileCount()); // 1
设计模式总结:
- 设计模式不是银弹,总结出抽象的模式相对比较简单,但应用起来却非常困难
- 现代编程语言的多编程范式带来更多的可能性
- 通过学习优秀的开源项目来学习设计模式,认真实践才能融会贯通