前端比较实用的设计模式总结
设计模式不分前端与后端,它是一种编程思想,无论在任何语言、任何环境中运行的程序,他们都会有一些相同的设计思路。而我们通过了解这些思想,才能提高自己的编程能力,写代码才会变成享受的事情。我们不仅仅只是在完成工作,更是在创造一件作品,一件属于你自己的而且令人赏心悦目的艺术品。 - 我自己的理解设计原则
【前端比较实用的设计模式总结】做什么事都需要遵循一些准则,设计模式也不例外。我们在设计一些设计模式时,一般遵循如下七项基本原则。
- 单一职责原则 (Single Responsibility Principle)
- 开放-关闭原则 (Open-Closed Principle)
- 里氏替换原则 (Liskov Substitution Principle)
- 依赖倒转原则 (Dependence Inversion Principle)
- 接口隔离原则 (Interface Segregation Principle)
- 最少知道原则(The Least Knowledge Principle)
- 组合/聚合复用原则 (Composite/Aggregate Reuse Principle)
单一职责原则 Single Responsibility Principle
- 一个对象或方法只做一件事情。
- 如果一个方法承担了过多的职责,那么在需求的变迁过程中,需要改写这个方法的可能性就越大。
- 应该把对象或方法划分成较小的粒度,提高代码可读性,提高系统可维护性。
- 对扩展开放:有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。
- 对修改封闭:一旦设计完成,模块的源代码不能被侵犯,任何人不允许修改已有源代码。
- 开放封闭的核心思想就是对抽象编程,而不对具体编程,因为抽象相对稳定。
- 让类依赖于固定的抽象,所以对修改就是封闭的;
- 而通过面向对象的继承和多态机制,可以实现对抽象体的继承,通过覆写其方法来改变固有行为,实现新的扩展方法,所以对于扩展就是开放的。
- 或者称迪米特法则(Law of Demeter),它描述了一种保持代码松耦合的策略,每个单元对其他单元只拥有有限的知识,只了解与当前单元紧密联系的单元;
- 现代面向对象程序设计语言通常使用 "." 作为访问标识,LoD 可以被简化为 "仅使用一个点(use only one dot)"。
- 也就是说,代码 a.b.Method() 违反了 LoD,而 a.Method() 则符合 LoD。打个比方,人可以命令一条狗行走,但是不应该直接指挥狗的腿行走,应该由狗去指挥它的腿行走。
在 JS 设计模式中,最核心的思想:封装变化。
将变与不变分离,确保变化的部分灵活、不变的部分稳定。
构造器模式 Constructor Pattern 特点:
- 构造器是一个当新建对象的内存被分配后,用来初始化该对象的一个特殊函数,在 JavaScript 中几乎所有的东西都是对象。
- 同时构造器可以使用的参数,以在第一次创建对象时,设置成员属性的方法的值。
- 对象定义:状态、属性、行为
function GirlFriend(name, age, cup, height) {
this.name = name;
this.age = age;
this.cup = cup;
this.height = height;
}GirlFriend.prototype.myIndentity = function () {
console.log(`我是被创造出来的女朋友,我叫${this.name}`);
};
const aGirl = new GirlFriend('小A女朋友', 28, '36F', 'LOL');
const bGirl = new GirlFriend('小B女朋友', 19, '36C', 'DOTA');
模块模式 Module Pattern 特点:
- 模块模式是前端最常用的模式,特别适合维护一段独立的代码片段。
- 模块模式几乎是所有JS流行的插件、框架的基础,比如jQuery、Backbone、Ember等。
- 可以创建私有(private)、公有(public)属性。
- 模块模式要利用 IIFE(立即调用函数表达式)和闭包来形成私有作用域。
const Exposer = (function () {
let privateVariable = 10;
const privateMethod = function () {
console.log('Inside a private method!');
privateVariable++;
}return {
first: function () {
console.log('This is a method I want to expose!');
},
second: function () {
privateMethod();
}
};
})();
Exposer.first();
// Output: This is a method I want to expose!
Exposer.second();
// Output: Inside a private method!
Exposer.privateMethod;
// undefined
单例模式 Singleton Pattern 特点:
- 单例模式限制实例的数量必须是一个,这个唯一的实例被称作单例(singleton)。
- 单例模式适合应用在一个广泛的系统范围内、在一个集中的地方来处理某些逻辑。
- 减少了全局变量的使用,从而清除了命名空间污染和命名冲突的风险。
- 例子:数据库连接池,连接池需要管理很多连接的生命周期,以便在将来需要对数据库发出请求时可以重用连接。
const Singleton = (function () {
let instance;
// 实例function init() {
return {
publicProperty: 'I am a property',
publicMethod: function () {
console.log('I am a mehtod');
},
};
}return {
getInstance: function () {
if (!instance) {
instance = init();
}
return instance;
}
};
})();
// 获取实例
const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();
适配器模式 Adapter Pattern 特点:
- 适配器模式可以将一个接口转换到另一种完全不同的接口,它允许接口不同的组件在一起工作。
- 例子:新研发出的组件和以往接口完全不同,可以通过适配器模式来兼容已有的老代码。
// 包裹计价 - 老服务
function Shipping() {
this.request = function (zipStart, zipEnd, weight) {
// ...
return "$49.75";
}
}// 包裹计价 - 新服务(带权限验证且逻辑解耦)
function AdvancedShipping() {
this.login = function (credentials) { /* ... */ };
this.setStart = function (start) { /* ... */ };
this.setDestination = function (destination) { /* ... */ };
this.calculate = function (weight) { return "$39.50";
};
}// 适配器
function ShippingAdapter(credentials) {
// 创建新服务
const shipping = new AdvancedShipping();
// 验证登录权限
shipping.login(credentials);
return {
// 保持老服务接口,内容逻辑适配到新服务
request: function (zipStart, zipEnd, weight) {
shipping.setStart(zipStart);
shipping.setDestination(zipEnd);
return shipping.calculate(weight);
}
};
}
// 调用老服务
const shipping = new Shipping();
const cost = shipping.request("78701", "10010", "2 lbs");
// 调用新服务代码
const credentials = { token: "30a8-6ee1" };
const adapter = new ShippingAdapter(credentials);
const cost = adapter.request("78701", "10010", "2 lbs");
工厂模式 Simple Factory Pattern 定义:
- 工厂模式,顾名思义,就是为了创造对象。
- 工厂模式类似于现实的工厂生产线,可以生产出大量类似的商品。
- 工厂模式的优点在于:能解决多个相似的问题,减少大量冗余代码。
const FactoryGirlFriend = function () {
function GirlFriend(name, age, cup, height, nationality) {
this.name = name;
this.age = age;
this.cup = cup;
this.height = height;
this.nationality = nationality
}
this.create = function (category) {
switch (category) {
case '御姐型':
return new GirlFriend('小A女朋友', 28, '36F', 170, '中国')
break;
case '萝莉型':
return new GirlFriend('小B女朋友', 19, '36C', 160, '日本')
break;
default:
throw new Error('参数错误')
}
};
}const aGirl = FactoryGirlFriend.create('御姐型');
const bGirl = FactoryGirlFriend.create('萝莉型');
策略模式 Strategy Pattern 特点:
- 策略模式定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。
- 策略模式可以有效避免很多if条件语句。
- 策略模式符合开放-封闭原则,使代码更容易理解和扩展。
- 策略模式中的代码可以复用。
const express = {
'顺丰': function (package) {
// 路径计算...
return "¥45.95";
},
'京东': function (package) {
// 路径计算...
return "¥39.40";
},
'韵达': function (package) {
// 路径计算...
return "¥24.45";
}
};
const calculateExpense = function (type, package) {
return express[type] ? express[type](package) : 0;
};
calculateExpense('京东', 1);
// ¥39.40
calculateExpense('韵达', 1);
// ¥24.45
观察者模式 Observer Pattern 特点:
- 观察者模式包含观察目标和观察者两类对象,
- 一个目标可以有任意数目的与之相依赖的观察者,
- 一旦观察目标的状态发生改变,所有的观察者都将得到通知。
// 定义一个主体对象
class Subject {
constructor() {
this.observers = [];
// 观察者
}
add(observer) { // 添加
this.observers.push(observer)
}
remove(observer) { // 移除
this.observers = this.observers.filter(item => item !== observer);
}
notify() { // 通知
this.observers.forEach(item => {
item.update();
})
}
}
// 定义观察着对象
class Observer {
constructor(name) {
this.name = name;
}
update() {
console.log(`my name is:${this.name}`);
}
}// 调用
const observer1 = new Observer('observer1');
const observer2 = new Observer('observer2');
const subject = new Subject();
subject.add(observer1);
subject.add(observer2);
subject.notify();
发布-订阅模式 Publish-subscribe Pattern 特点:
- 发布 + 订阅 = 观察者模式 ?
- 在观察者模式中,观察者是知道Subject的,Subject一直保持对观察者进行记录。然而,在发布订阅模式中,发布者和订阅者不知道对方的存在,它们只有通过消息代理进行通信。
- 在发布订阅模式中,组件是松散耦合的,正好和观察者模式相反。
const publisher = {
topics: [], // 可订阅的主题
subscribe(key, fn) { // 订阅
if (!this.topics[key]) {
this.topics[key] = [];
}
this.topics[key].push(fn);
},
unSubscribe(key, fn) { // 删除
let fns = this.topics[key];
for (let i = 0;
i < fns.length;
++i) {
if (fns[i] === fn) {
fns.splice(i, 1);
}
}
},
publish(...args) { // 发布
let key = args.shift();
let fns = this.topics[key];
if (!fns || fns.length <= 0) return;
for (let i = 0, len = fns.length;
i < len;
i++) {
fns[i].apply(this, args);
}
}
}// 调用
publisher.subscribe('name', (name) => {
console.log(`your name is ${name}`);
})
publisher.subscribe('gender', (gender) => {
console.log(`your gender is ${gender}`);
})
publisher.publish('name', '奥特曼');
// your name is 奥特曼
publisher.publish('gender', '男');
// your gender is 男
示例代码:发布-订阅模式在vue中的作用
// 遍历传入实例的data对象的属性,将其设置为Vue对象的访问器属性
function observe(obj, vm) {
Object.keys(obj).forEach(function (key) {
defineReactive(vm, key, obj[key]);
});
}
// 设置为访问器属性,并在其getter和setter函数中,使用订阅发布模式。互相监听。
function defineReactive(obj, key, val) {
// 这里用到了观察者模式,它定义了一种一对多的关系,让多个观察者监听一个主题对象,
// 这个主题对象的状态发生改变时会通知所有观察者对象,观察者对象就可以更新自己的状态。
// 实例化一个主题对象,对象中有空的观察者列表
let dep = new Dep();
// 将data的每一个属性都设置为Vue对象的访问器属性,属性名和data中相同
// 所以每次修改Vue.data的时候,都会调用下边的get和set方法。然后会监听v-model的input事件,
// 当改变了input的值,就相应的改变Vue.data的数据,然后触发这里的set方法
Object.defineProperty(obj, key, {
get: function () {
// Dep.target指针指向watcher,增加订阅者watcher到主体对象Dep
if (Dep.target) {
dep.addSub(Dep.target);
}
return val;
},
set: function (newVal) {
if (newVal === val) {
return
}
val = newVal;
// console.log(val);
// 给订阅者列表中的watchers发出通知
dep.notify();
}
});
}// 主题对象Dep构造函数
function Dep() {
this.subs = [];
}
// Dep有两个方法,增加订阅者和发布消息
Dep.prototype = {
addSub: function (sub) {
this.subs.push(sub);
},
notify: function () {
this.subs.forEach(function (sub) {
sub.update();
});
}
}
代理模式 Proxy Pattern 特点:
- 为其他对象提供一种代理以控制对这个对象的访问,客户端甚至感知不到代理层的存在。
- 代理对象可以代替本体对象被实例化,此时本体对象未真正实例化,等到合适时机再实例化。
- 代理模式可以延迟创建开销很大的本体对象,他会把本体的实例化推迟到有方法被调用时。
// 普通模式
const myImage = (function () {
const imgNode = document.createElement('img');
return {
setSrc: function (src, id) {
imgNode.src = https://www.it610.com/article/src;
document.getElementById(id).appendChild(imgNode);
}
}
})();
// 普通模式加载图片
myImage.setSrc('https://tengmed.com/a.png', "avatar");
// 代理模式
const ProxyImage = (function () {
const img = new Image();
img.onload = function () {
myImage.setSrc(this.src, this.id);
};
return {
setSrc: function (src, id) {
myImage.setSrc('http://loading.gif', id);
// 添加默认loading图片
img.src = https://www.it610.com/article/src;
img.id = id;
}
}
})();
// 代理模式加载图片
ProxyImage.setSrc('https://tengmed.com/a.png', "avatar");
示例代码:缓存代理
let mult = function () {
let a = 1;
for (let i = 0, ilen = arguments.length;
i < ilen;
i += 1) {
a = a * arguments[i];
}
return a;
};
// 计算加法
let plus = function () {
let a = 0;
for (let i = 0, ilen = arguments.length;
i < ilen;
i += 1) {
a += arguments[i];
}
return a;
}
// 代理函数
let proxyFunc = function (fn) {
let cache = {};
// 缓存对象
return function () {
let args = Array.prototype.join.call(arguments, ',');
if (args in cache) {
return cache[args];
// 使用缓存代理
}
return cache[args] = fn.apply(this, arguments);
}
};
let proxyMult = proxyFunc(mult);
console.log(proxyMult(1, 2, 3, 4));
// 24
console.log(proxyMult(1, 2, 3, 4));
// 缓存取 24let proxyPlus = proxyFunc(plus);
console.log(proxyPlus(1, 2, 3, 4));
// 10
console.log(proxyPlus(1, 2, 3, 4));
// 缓存取 10
推荐阅读
- 关于QueryWrapper|关于QueryWrapper,实现MybatisPlus多表关联查询方式
- MybatisPlus使用queryWrapper如何实现复杂查询
- 汇讲-勇于突破
- python学习之|python学习之 实现QQ自动发送消息
- 科学养胃,别被忽悠,其实真的很简单
- 其实你就是个普通人
- 海院(实干是海院风景(上))
- 你眼里的不公平,其实很公平
- 孩子不是实现父母欲望的工具——林哈夫
- opencv|opencv C++模板匹配的简单实现