*设计原则
(1)单一职责原则(SRP)
一个对象或方法只做一件事情。如果一个方法承担了过多的职责,那么在需求的变迁过程中,需要改写这个方法的可能性就越大。
(2)最少知识原则(LKP)
如:老板要统计每个人一个月的代码量,但是不需要知道具体写的内容。
(3)开放-封闭原则(OCP)
// bad
function Eat() {
}
Eat.prototype.eat1 = function () {
console.log('吃肉!');
}
Eat.prototype.eat2 = function () {
console.log('吃饭!~');
}
function eat() {
var eater = new Eat();
eater.eat1();
eater.eat2();
}
// good
function eatFood(food) {
console.log(food);
}
function ieat(){
eatFood('吃肉');
eatFood('吃鱼。。。。');
eatFood('吃素');
}
/**
*
* 开闭原则:软件实体(类、模块、函数)等应该是可以 扩展的,但是不可修改。
* 当需要改变一个程序的功能或者给这个程序增加新功能的时候,可以使用增加代码的方式,
* 尽量避免改动程序的源代码,防止影响原系统的稳定
*/
六,创建型设计模式
目的:指导我们解决创建对象的方面的需求和问题。
用处:当我们在设计功能的类怎么设计,怎么创建对象的时候用上。
1),工厂模式
目的:让我们不用关心对象的创建,更简单的获取到想要的对象。
应用场景:我们需要创建大量的对象,尤其当可能是各种对象的时候。
function factory(type) {
return new factory[type]();
}
factory.basketball = function () {
}
factory.tennis = function () {
}
factory('basketball');
*构建一个工厂方法,通过调用这个工厂方法,来创建类,而不用我们具体去创建某个类。其实是一个依赖于抽象不依赖于具体的实现。
例子:创建不同的弹窗;jQuery的实现;
// bad
function WarnPop(word) {
this.color = 'red';
this.word = word;
}
WarnPop.prototype.WarnPopaction = function () {
}
function SuccessPop(word) {
this.color = 'green';
this.word = word;
}
SuccessPop.prototype.SuccessPopaction = function () {
}
new WarnPop();
new SuccessPop();
// good
function popFactory(type, word) {
}
popFactory.warnPop = function () {
}
popFactory.warnPop.prototype.warnPopaction = function () {
}
popFactory.successPop = function () {
}
popFactory.successPop.prototype.successPopaction = function () {
}
new popFactory('warnPop','warning');
console.log(new popFactory('warnPop','warning'))
2)建造者模式
目的:把对象的构建方案分离出来,使得同样的构建过程可以创建不同的表示
应用场景:构建一个需要根据参数灵活配置的复杂对象。
function bulider() {
this.part1 = new buliderMode1();
this.part2 = new buliderMode2();
}
// 构成部分
function buliderMode1() {
}
function buliderMode2() {
}
*把最终的类分成不同的组成部分,独立出去,然后在组合在一起构建成最终的类。
例子:创建一个灵活的新闻;webpack的思路;
// bad
function NewsnoPic() {
}
function NewsnoDescript() {
}
function NewsAll() {
}
// good
function content() {
}
function pic() {
}
function title() {
}
function news(config) {
config.content ? this.content = new content(config.content) : null
config.pci ? this.pci = new pic(config.pci) : null
config.title ? this.title = new title(config.title) : null
}
// a
new news({
pci:'XXXXXXXX',
title:'我是标题'
})
// b
new news({
pci:'XXXXXXXX'
})
3)单例模式
目的:让你的类只能实例化一次。
应用场景:当我们创建的对象如果存在两个会出现问题
function single(a) {
if (single.instanced) {
return single.instanced;
} else {
this.a = a;
single.instanced = this
}
}
single.instanced = null;
*通过一个标识,来判断是否已经实例化,如果已经实例化过一次,这个标识就会标识已经实例化过了。
例子:创建一个唯一的储存器;vue-router;
// 储存器
function store(a) {
this.cache = {}
if (store.installed) {
return store.installed;
} else {
this.cache = {}
store.installed = this;
}
}
store.prototype.addData = function (name, value) {
this.cache[name] = value;
}
store.installed = null;
// vue-router
// Vue.use(vue-router)
let _vue;
function install(vue) {
if (_vue === vue && install.installed) return;
// 注册vue-router
_vue = vue;
install.installed = true;
}
七,结构型设计模式
目的:指导我们怎么组织对象的模块
用处:当我们在设计对象怎么组织,模块间是怎么样得结构时使用。
1)装饰者模式
目的:帮我们更好的去扩展方法,代替继承去更好的扩展功能
应用场景:当我们需要扩展功能又不好直接去修改的时候
// 原方法
function a() {
console.log(1);
}
// 扩展a得congsole.log(2)功能
function extenda() {
a();
console.log(2);
}
/**
* 现在a方法要扩展,我们有两个选择,要么直接重写a,但是违反开闭原则。
* 那么我们可以通过装饰者三部走:
* 1,重写新方法;
* 2,调用老方法;
* 3,加上新操作。
*/
例子:扩展事件绑定;vue的数组监听;
/**
* 1,找到老代码,改
* 2,直接重写
*/
// 1,写一个新的方法;2,判断之前是否有方法;3,保存老方法;4,执行老方法;5,执行新方法;
var decorator = function (dom, fn) {
if (typeof dom.onclick == 'function') {
var __old = dom.onclick;
dom.onclick = function () {
__old();
fn();
}
}
}
decorator(dom, function () {
console.log('删除成功!');
});
// vue数组监听
var arrProto = Array.prototype;
var arraryMethods = Object.create(arrProto);
var arr = ['push', 'pop', 'shift', 'unshift', 'sort', 'reverse', 'splice'];
arr.forEach((method) => {
arraryMethods[method] = function () {
var original = arrProto[method];
var result = original.apply(this, args);
dep.notify();//vue更新视图的方法
return result;
}
})
//在需要扩展一个功能,把一个方法进行扩展的时候但是又不方便直接修改原方法,这个时候就可以使用装饰者模式
2)适配器模式
目的:当接口名字发生了改变,通过写一个适配器,来替代替换。
应用场景:面临接口不通用的问题。
var a = function (){
b();
}
// 我们原本在所有代码里面都调用的a方法,突然有一天,有人把a方法的名字改成了b,
// 但是我们已经很多地方调用了a方法了,怎么办?
例子:
(1)框架的变更:目前项目使用的A框架,现在改成了jQuery,两个框架十分类似,但是有少数几个方法不同。
(2)参数的适配:为了避免参数不适配产生问题,很多框架会有一个参数适配操作。
window.A = $;
// $.css---->A.c
// A.o---->$.on
A.c = function () {
return $.css();
}
/**
*
* @param {*} obj
* obj对象中的name属性必传,不传操作undefined就会报错,此时可以使用参数适配
*/
function f1(obj) {
var _default = {
name: 'XXXXX',
age: 18
}
for (var item in _default) {
obj[item] = obj[item] || _default[item];
}
}
// vue很多必传参数没有传也没有报错也是使用这个适配器模式
3)桥接模式
目的:抽象出代码中的公用部分,提高代码的复用性。
应用场景:发现写了一大堆核心功能一致,但有一些细节不同的方法的时候。
function a(){
console.log(1);
console.log(2);
}
function b(){
console.log(1);
console.log(3);
}
function c(){
console.log(1);
console.log(4);
}
// 提取出来
function common(){
console.log(1);
}
/**
* a,b,c三个方法都有一个相同的console.log(1)操作,那么我们就把console.log(1)
* 独立出来,在桥接回去。
*/
例子:
(1),创建不同的选中效果:有一组菜单,上面每种选项都有不同的选中效果。
(2),Axios中有很多发请求的方法,那么这些方法是怎么定义的呢?
// bad
function createMenu1() {
var _dom = document.createElement('div');
_dom.innerHTML = 'this is menu1~~';
_dom.onmouseover = function () {
_dom.style.backgroundColor = 'red';
}
}
function createMenu2() {
var _dom = document.createElement('div');
_dom.innerHTML = 'this is menu2~~';
_dom.onmouseover = function () {
_dom.style.backgroundColor = 'white';
}
}
function createMenu3() {
var _dom = document.createElement('div');
_dom.innerHTML = 'this is menu3~~';
_dom.onmouseover = function () {
_dom.style.color = 'green';
}
}
// good
function createMenu(word, action) {
var _dom = document.createElement('div');
_dom.innerHTML = word;
_dom.onmouseover = function () {
action();
}
}
var data = [
{
word: 'this is menu1~~',
action: function () {
_dom.style.backgroundColor = 'red';
}
},
{
word: 'this is menu2~~',
action: function () {
_dom.style.backgroundColor = 'white';
}
},
{
word: 'this is menu3~~',
action: function () {
_dom.style.color = 'green';
}
}
]
data.forEach((menu) => {
createMenu(menu.word, menu.action)
})
// axios 例子
// 注册post,put,patch。。。
axios.prototype.post = function () {
// 发请求
}
axios.prototype.put = function () {
// 发请求
}
axios.prototype.patch = function () {
// 发请求
}
var arr = ['post', 'put', 'patch'];
// this.request:就是post。。。方法提取出来的发请求的核心操作,
arr.forEach((method) => {
axios.prototype[method] = function (url, data, config) {
// merge是一个对象的合并工具方法,这里不一一实现,config有就合并,没有则为一个空对象
return this.request(merge(config || {}, {
method: method,
data: data,
url: url
}))
}
})
/**
* 总结
* 桥接模式:我们写了很多类似方法的时候,把核心操作提取出来,然后共同的方法去调用核心操作,
* 在不同的细节点通过参数的传入/判断来解决细节上的不同
*/
4)享元模式
目的:减少重复代码块和重复对象。(化多为一)
应用场景:发现出现了类似重复的代码块和对象。
享元模式与桥接模式的区别:
桥接模式是把代码中相同的部分抽象出来作为独立方法而享元模式的是把相同部分的不同点作为一个享元数据
// 假设要创建十个不同大小的圆形对象
function circle(radius) {
this.radius = radius;
}
var c1 = new circle(10);
var c2 = new circle(30);
var c3 = new circle(50);
// .....
// 分析,这几个圆形他们只是半径不同
var radiusArr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
// 把原型的创建作为类的一个方法
circle.prototype.createCircle = function (radius) {
}
radiusArr.forEach((radius) => {
new circle().createCircle(radius);
})
// 把几个相似对象或者代码块的不同部分拿出来作为享元数据,然后剩下相同部分共享这些享元数据。
例子
(1)文件上传:项目中有一个文件上传功能,该功能可以上传多个文件
(2)if-else分支写着类似操作:我们常常发现一段if-else分支写着类似的代码。
// bad
function uploader(file) {
this.file = file;
}
uploader.prototype.up = function () {
}
new uploader(filebj).up(); //n个文件就需要new uploader n多次
// good
var fileArr = ['file1', 'file2', 'file3'];
function uploader() {
}
uploader.prototype.uploaderFile = function (filter) {
// 文件上传的方法
}
fileArr.forEach(file => {
new uploader.uploaderFile(file); //只需要new一次
})
// if-else例子
function f1(num) {
var all = 0;
if (num) {
all += num;
} else {
all += 1;
}
}
function f2(num) {
var all = 0;
var _num = num ? num : 1;
all += _num;
}
// extend 例子
$.extend({ a: 1 });
$.extend({ a: 1 }, { b: 2 });
$.extend = function () {
if (arguments.length === 1) {
for (var item in arguments) {
this[item] = arguments[0][item];
}
} else if (arguments.length === 2) {
for (var item in arguments[1]) {
arguments[0][item] = arguments[1][item];
}
}
}
$.extend = function () {
var target = arguments[0];
var source = this;
// if (arguments.length === 2) {
// target = arguments[1];
// source = arguments[0];
// }
for (var i = 1; i < arguments.length; i++) {
target = arguments[i];
source = arguments[i - 1];
}
for (var item in target) {
source[item] = target[0][item];
}
}
/**
5)外观模式
目的:简化多个子系统的调用
应用场景:有一个功能需要多个模块调用来完成的时候,我们提供一个集合的高级接口,让使用者只需要调用这个高级接口。
// 完成功能需要调用a,b,c方法
a();b();c();
// 为了简化调用,提供高级接口
function d(){
a();b();c();
}
/**
* 为了简化使用者的调用,把一大堆的子系统手动调用,封装为一个更高级的接口,
* 使用者只需要调用一个接口即可。
*/
例子
(1)兼容的封装:在需要兼容的时代,我们需要判断浏览器支持什么来做对应的操作;
(2)一系列复杂的绘图命令:Canvas如果要画一个图形调用api很复杂,为了简化,我们提供一个高级接口。
// 每一个都这样写重复的代码,浪费代码体积等
if (dom.addEventListener) {
dom.addEventListener(type, fn);
} else if (dom.attachEvent) {
dom.attachEvent('on' + type, fn);
} else {
dom["on" + type] = fn;
}
// good
function addEvent(dom, type, fn) {
if (dom.addEventListener) {
dom.addEventListener(type, fn);
} else if (dom.attachEvent) {
dom.attachEvent('on' + type, fn);
} else {
dom["on" + type] = fn;
}
}
addEvent();
// canvas例子
// 画图
ctx.beginPath();
ctx.arc(95, 50, 40);
ctx.stroke();
// 画三角形
ctx.beginPath();
ctx.moveTo(250, 50);
ctx.lineTo(200, 200);
ctx.lineTo(300, 300);
ctx.closePath();
ctx.stroke();
// 画圆的方法
function circle(paint, r) {
// 画图
ctx.beginPath();
ctx.arc(paint[0], paint[1], r);
ctx.stroke();
}
circle([10, 20], 10)
// 画三角形的方法
function triangle(point1, point2, point3) {
ctx.beginPath();
ctx.moveTo(point[0], point1[1]);
ctx.lineTo(point2[0], point2[1]);
ctx.lineTo(point3[0], point3[1]);
ctx.closePath();
ctx.stroke();
}
/**
* 总结
* 外观模式:简化调用,让我们能够不用一个个手动去调用,只需要调用一个高级接口就可以完成需求。
*/
八),观察者 与 发布订阅
* 特殊说明:
* + 观察者 和 发布订阅 两个模式
* + 目前的市场上,有一部分人认为是一个,一部分人认为两个
* + 我认为是两个
*
* 观察者模式:
* + 例子:监控
* ===》我们坐在教室里面 就是被观察者
* ===》监控后面的老师 就是 观察者
* ===》当被观察者触发了一些条件的时候,观察者就会触发一定的技能
* + 观察者模式:监控一个 对象 的状态,一旦状态发生变化,马上触发技能
* =》 需要两个构造函数来实现
* 1,创建被观察者
* =》 属性,自己的状态
* =》 队列,记录都有谁观察着自己 数组[]
* =》 方法,设置自己状态,当我需要改变的时候,要触发这个方法改变状态。
* =》 方法,添加观察者
* =》 方法,删除观察者
* 2,创建观察者
* =》创建一个身份证明
* =》需要一个技能
// 观察者 构造函数
class Observer {
constructor(name, fn = () => {}) {
this.name = name
this.fn = fn
}
}
// 创建两个观察者
const bzr = new Observer('班主任', (state) => {
console.log('因为:' + state + '把你爸找来!');
});
const njzr = new Observer('年级主任', (state) => {
console.log('因为:' + state + '你是哪个班的!');
})
const xz = new Observer('校长', (state) => {
console.log('因为:' + state + '骂你的班主任!');
})
console.log(bzr, xz);
class Subject {
constructor(state) {
// 记录自己的状态
this.state = state;
// 数组用来保存观察着我的人
this.observers = [];
}
// 设置自己的状态
setState(val) {
this.state = val;
// 当状态发生变化,就需要把我的观察者的技能都触发
// 遍历this.observers 依次触发技能
this.observers.forEach(item => {
// 告诉他我改变成了什么状态
console.log(item)
item.fn(this.state);
this.delObserver(item);
})
}
// 添加观察者
addObserver(obs) {
// 谁是观察者,就传谁进来
// 先判断观察者是否存在,如果已经存在,就不在添加了
// const res = this.observers.some(item => item === obs);//true
// const res = this.observers.find(item => item === obs);//对象
// const res = this.observers.indexOf(obs)//下标
// const res = this.observers.findIndex(item => item === obs) //下标
// const res = this.observers.filter(item => item === obs); //数组内有就是重复的对象
this.observers = this.observers.filter(item => item !== obs);
this.observers.push(obs);
}
// 删除观察者
delObserver(obs) {
// 把obs观察者删除就可以
this.observers = this.observers.filter(item => item !== obs);
}
}
// 创造一个被观察者
const xiaoming = new Subject("学习");
const xiaohong = new Subject("学习");
// 给小明添加一个观察者
xiaoming.addObserver(bzr);
xiaoming.addObserver(xz);
xiaoming.addObserver(bzr);
xiaoming.addObserver(bzr);
console.log(xiaoming);
xiaoming.setState('玩游戏!!!!')
console.log('----', xiaoming);
// 给小红添加一个观察者
xiaohong.addObserver(bzr);
xiaohong.addObserver(xz);
xiaohong.addObserver(njzr);
发布订阅模式
+ 有一个对象,有人一直看着他
+ 当这个对象发生变化的时候,第三方通知这个看着的人,触发技能
+ 例子:买书
1,普通程序员买书
=》去书店,问,没有
=》回家
=》过一会再去,再问,没有,回家
=》过一会再去,再问,没有,回家
2,一个发布订阅的程序员
=》去书店,问,没有,留下一个联系方式给店员
=》一旦有了书,就会接到电话
=》触发技能去买书
+ 只需要一个构造函数
=》 创建一个第三方店员的身份(addEventListener)
=》 模拟一个 addEventListener
分析构造函数
+ 属性:任务队列(消息队列)
{
click: [fn1, fn2],
abc:[fn1, fn2, fn3]
}
+ 方法:能向消息队列里面添加内容
+ 方法:删除消息队列里面的内容
+ 方法:触发消息队列里面的内容
// 创建一个第三方观察者构造函数
class Observer {
constructor() {
this.message = {}
}
// 1,向消息队列里面添加内容
on(type, fn) {
// type 我拜托你看着的行为
// fn 我要你在行为发生的时候要做的事情
// 就应该把这些写在消息队列里面
// this.message[type] = [fn]
// 1,判断 message 里面有没有这个行为已经被注册过了
// 如果没有,我应该给你赋值这个行为,值赋值为数组[]
// 如果有,直接向数组里面添加就可以
if (!this.message[type]) { //消息队列里面还没有注册过
this.message[type] = []
}
this.message[type].push(fn);
}
// 2,删除消息队列里面的内容
off(type, fn) {
// 判断 如果 fn 不存在,只有一个参数的情况
if (!fn) {
// 直接把整个事情取消掉
// 从message 里面把 a 成员删除掉,就好了
delete this.message[type];
return
}
// 代码能来带这里表示 fn 存在
// 判断你是不是真的订阅过
if (!this.message[type]) return;
// 你确实订阅过,我就可以使用 filter 删除
this.message[type] = this.message[type].filter(item => item !== fn);
}
// 3,触发消息队列
trigger(type) {
// 先判断是否订阅过
if (!this.message[type]) return;
// 找到这个数组,把里面的每一个函数触发
this.message[type].forEach(item => {
// item 就是每一个函数
item();
});
}
}
const person1 = new Observer();
console.log(person1)
// 当你拜托这个 person1 帮你观察一些内容的时候
// 告诉你一个行为,当行为出现的时候,告诉你干什么
person1.on('JavaScript', handlerA);
person1.on('JavaScript', handlerB);
person1.on('#C', handlerA);
person1.on('PHP', handlerB);
person1.on('PHP', handlerC);
person1.on('PHP', handlerD);
person1.on('JAVA', handlerA);
person1.on('JAVA', handlerE);
// 删除
// 告诉 person1 ,我这个事情不用你管了
// 1,我只告诉你这个事情不用你管了
// person1.off('JavaScript'); // 把 消息队列 里面属于a的数组清空掉
person1.off('JavaScript', handlerA); //告诉你 a 发生的时候,不用做 handlerA 这个事情了
// person1 这个人一旦触发 a 行为,就要把后面的所有的事件处理函数执行掉
person1.trigger('JavaScript')
person1.trigger('PHP')
function handlerA() {
console.log('handlerA')
}
function handlerB() {
console.log('handlerB')
}
function handlerC() {
console.log('handlerC')
}
function handlerD() {
console.log('handlerD')
}
function handlerE() {
console.log('handlerE')
}
区别:观察者模式中观察者和目标直接进行交互,而发布订阅模式中统一由调度中心(观察者)进行处理,订阅者和发布者互不干扰。这样一方面实现了解耦,还有就是可以实现更细粒度的一些控制。比如发布者发布了很多消息,但是不想所有的订阅者都接收到,就可以在调度中心做一些处理,类似于权限控制之类的。还可以做一些节流操作。
策略模式:
+ 一个问题匹配多个解决方案,不一定要用到哪一个
+ 而且有可能随时还会继续新增多个方案
+ 例子:购物车结算
=》我们有好多种折扣计算方式
=》满 100 减 10
=》满 200 减 25
=》8折
=》7折
1,把我们的多种方案,已闭包的形式保存起来
+ 对外留一个接口
+ 可以添加和减少
// 接收两个参数:价格,折扣种类
function calcPrice(price, type) {
if (type === '100_10') {
price -= 10;
} else if (type === '200_25') {
price -= 25;
} else if (type === '80%') {
price *= 0.8;
} else if (type === '70%') {
price *= 0.7;
} else {
console.log('没有这种折扣')
}
return price;
}
// 使用的时候
const res = calcPrice(320, '100_10');
console.log('res--', res)
/*
以上 一旦加一种折扣 或者删掉一种折扣 就要对 calcPrice 进行修改 加上/删除 else if
*/
// 策略模式优化:
// 以闭包的形式把折扣方案保存下来
const g_calcPrice = (function () {
const sale = {
'100_10': function (price) {
return price -= 10
},
'200_25': function (price) {
return price -= 25
},
'80%': function (price) {
return price *= 0.8
},
}
// return 出去的函数,才是 g_calcPrice 本体
// return function g_calcPrice(price, type) {
// // 1,判断对象里面有没有这个折扣类型
// // 2,如果有,你们就执行
// // 3,如果没有,那么就告诉他没有这个折扣类型
// if (!sale[type]) return '没有这个折扣类型'
// // 找到了 sale 里面指定的那个函数 执行计算结果,返回给外边
// return sale[type](price);
// }
function g_calcPrice(price, type) {
// 1,判断对象里面有没有这个折扣类型
// 2,如果有,你们就执行
// 3,如果没有,那么就告诉他没有这个折扣类型
if (!sale[type]) return '没有这个折扣类型'
// 找到了 sale 里面指定的那个函数 执行计算结果,返回给外边
return sale[type](price);
}
// 把函数当做一个对象,往对象里面添加一些成员
g_calcPrice.add = function (type, fn) {
// 1,专门用来添加折扣
// 2,判断这个折扣是不是存在
if (sale[type]) return '这种折扣已存在!'
sale[type] = fn
return '添加成功!'
}
// 删除一个折扣
g_calcPrice.del = function (type) {
delete sale[type];
console.log('删除成功')
}
return g_calcPrice
})();
const g_res = g_calcPrice(320, '100_10');
console.log('g_res:', g_res);
// 添加折扣
g_calcPrice.add('70%', function (price) {
return price *= 0.7
})
console.log('测试添加折扣:', g_calcPrice(320, '70%'));
// 删除折扣
g_calcPrice.del('70%')
发布于 2021-07-17 00:15