1. 设计模式:
通常在我们解决问题的时候,很多时候不是只有一种方式,我们通常有多种方式来解决;但是肯定会有一种通用且高效的解决方案,这种解决方案在软件开发中我们称它为设计模式;
设计模式并不是一种固定的公式,而是一种思想,是一种解决问题的思路;恰当的使用设计模式,可以实现代码的复用和提高可维护性;
1. 单例模式
1. 定义
保证一个类仅有一个实例,并提供一个访问它的全局访问点
2. 核心
确保只有一个实例,并提供全局访问
3. 实现
假设要设置一个管理员,多次调用也仅设置一次,我们可以使用闭包缓存一个内部变量来实现这个单例
class SingletonSetManager {
constructor(name) {
this.name = name;
this.instance = null;
}
static getInstance(name) {
if(!this.instance) {
this.instance = new SingletonSetManager(name);
}
return this.instance;
}
}
let person1 = SingletonSetManager.getInstance("a");
console.log(person1.name); // a
let person2 = SingletonSetManager.getInstance("b");
console.log(person2.name); // a
2. 策略模式
1. 定义
根据不同参数可以命中不同的策略
2. 核心
把多个函数封装到一个对象
通过不同参数调用不同的方法
3. 实现
比如一个超市促销开始商品打折,A类商品9折,B类商品8折,C类商品7折
let rule = {
"A": function(price) {
return price * 0.9
},
"B": function(price) {
return price * 0.8
},
"C": function(price) {
return price * 0.7
}
}
let sale = (product,price) => {
return rule[product](price);
}
console.log(sale("A",100)); // 90
3. 代理模式
1. 定义
为其他对象提供一种代理以控制对这个对象的访问,为了不暴露执行对象的部分代码
2. 核心
不直接访问函数,提供一个替身函数,先访问替身函数切做一些处理后再调用原函数
3. 实现
代理模式主要有三种:保护代理、虚拟代理、缓存代理
保护代理:
主要实现了访问主体的限制行为,以过滤字符作为简单的例子
// 主体,发送消息
function sendMsg(msg) {
console.log(msg);
}
// 代理,对消息进行过滤
function proxySendMsg(msg) {
// 无消息则直接返回
if (typeof msg === 'undefined') {
console.log('msg is undefined');
return;
}
// 有消息则进行过滤
msg = ('' + msg).replace(/(泥马)*(草)*\s*/g, '');
sendMsg(msg);
}
sendMsg('我喜欢草泥马 你呢'); // 我喜欢草泥马 你呢
proxySendMsg('我喜欢草泥马 你呢'); // 我喜欢你呢
proxySendMsg(); // msg is undefined
虚拟代理:
在控制对主体的访问时,加入了一些额外的操作
在滚动事件触发的时候,也许不需要频繁触发,我们可以引入函数节流,这是一种虚拟代理的实现
// 函数防抖,频繁操作中不处理,直到操作完成之后(再过 delay 的时间)才一次性处理
function debounce(fn, delay) {
delay = delay || 200;
var timer = null;
return function() {
var arg = arguments;
// 每次操作时,清除上次的定时器
clearTimeout(timer);
timer = null;
// 定义新的定时器,一段时间后进行操作
timer = setTimeout(function() {
fn.apply(this, arg);
}, delay);
}
};
var count = 0;
// 主体
function scrollHandle(e) {
console.log(e.type, ++count); // scroll
}
// 代理
var proxyScrollHandle = (function() {
return debounce(scrollHandle, 500);
})();
window.onscroll = proxyScrollHandle;
缓存代理:
可以为一些开销大的运算结果提供暂时的缓存,提升效率
举个栗子,缓存加法操作
// 主体
function add() {
var arg = [].slice.call(arguments);
return arg.reduce(function(a, b) {
return a + b;
});
}
// 代理
var proxyAdd = (function() {
var cache = Object.create(null);
return function() {
var arg = [].slice.call(arguments).join(',');
// 如果有,则直接从缓存返回
if (cache[arg]) {
return cache[arg];
} else {
var ret = add.apply(this, arguments);
cache[arg] = ret;
return ret;
}
};
})();
console.log(
add(1, 2, 3, 4),
add(1, 2, 3, 4),
proxyAdd(10, 20, 30, 40),
proxyAdd(10, 20, 30, 40)
); // 10 10 100 100
4. 迭代器模式
1. 定义
提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。
2. 核心
- 为遍历不同数据结构的 “集合” 提供统一的接口;
- 能遍历访问 “集合” 数据中的项,不关心项的数据结构;
3. 实现
1. 内部迭代器
内部已经定义好了迭代规则,它完全接手整个迭代过程,外部只需要一次初始调用
let each = function(arr, callback) {
for(var i = 0; i < arr.length; i++) {
callback(i, arr[i]);
}
}
each([1,2,3,4,5],function(i, item) {
console.log(i, item);
})
优点:调用方式简单,外部仅需一次调用
缺点:迭代规则预先设置,欠缺灵活性。无法实现复杂遍历需求(如: 同时迭代比对两个数组)
2. 外部迭代器
外部显示(手动)地控制迭代下一个数据项
借助 ES6 新增的 Generator 函数中的 yield* 表达式来实现外部迭代器。
// ES6 的 yield 实现外部迭代器
function* generatorEach(arr) {
for (let [index, value] of arr.entries()) {
yield console.log([index, value]);
}
}
let each = generatorEach(['Angular', 'React', 'Vue']);
each.next();
each.next();
each.next();
// 输出:[0, 'Angular'] [1, 'React'] [2, 'Vue']
优点:灵活性更佳,适用面广,能应对更加复杂的迭代需求
缺点:需显示调用迭代进行(手动控制迭代过程),外部调用方式较复杂
5. 发布订阅模式(观察者模式)
1. 定义
也称作观察者模式,定义了对象间的一种一对多的依赖关系,当一个对象的状态发 生改变时,所有依赖于它的对象都将得到通知,发布订阅模式与观察者模式的区别是观察者模式中观察者与被观察者之间是有依赖关系的,发布订阅模式没有。
2. 核心
-
取代对象之间硬编码的通知机制,一个对象不用再显式地调用另外一个对象的某个接口。
-
与传统的发布-订阅模式实现方式(将订阅者自身当成引用传入发布者)不同,在JS中通常使用注册回调函数的形式来订阅
3. 实现
- 首先要想好谁是发布者(比如下面的卖家)。
- 然后给发布者添加一个缓存列表,用于存放回调函数来通知订阅者(比如下面的买家收藏了卖家的店铺,卖家通过收藏了该店铺的一个列表名单)。
- 最后就是发布消息,发布者遍历这个缓存列表,依次触发里面存放的订阅者回调函数。
var Event = (function(){
var list = {},
listen,
trigger,
remove;
listen = function(key,fn){
if(!list[key]) {
// 如果还没有订阅过此类消息,给该类消息创建一个缓存列表
list[key] = [];
}
list[key].push(fn); // 订阅消息添加到缓存列表
};
trigger = function(){
var key = Array.prototype.shift.call(arguments), // 取出消息类型名称
fns = list[key]; // 取出该消息对应的回调函数的集合
// 如果没有订阅过该消息的话,则返回
if(!fns || fns.length === 0) {
return false;
}
for(var i = 0, fn; fn = fns[i++];) {
fn.apply(this,arguments); // arguments 是发布消息时附送的参数
}
};
remove = function(key,fn){
// 如果key对应的消息没有订阅过的话,则返回
var fns = list[key];
// 如果没有传入具体的回调函数,表示需要取消key对应消息的所有订阅
if(!fns) {
return false;
}
if(!fn) {
fns && (fns.length = 0);
}else {
for(var i = fns.length - 1; i >= 0; i--){
var _fn = fns[i];
if(_fn === fn) {
fns.splice(i,1);// 删除订阅者的回调函数
}
}
}
};
return {
listen: listen,
trigger: trigger,
remove: remove
}
})();
// 测试代码如下:
Event.listen("color",function(size) {
console.log("尺码为:"+size); // 打印出尺码为42
});
Event.trigger("color",42);
优点:
- 支持简单的广播通信,当对象状态发生改变时,会自动通知已经订阅过的对象。
- 发布者与订阅者耦合性降低,发布者只管发布一条消息出去,它不关心这条消息如何被订阅者使用,同时,订阅者只监听发布者的事件名,只要发布者的事件名不变,它不管发布者如何改变。
缺点:
创建订阅者需要消耗一定的时间和内存。如果过度使用的话,反而使代码不好理解及代码不好维护等等。
6. 工厂模式
1. 定义
-
所谓工厂模式就是像工厂一样重复的产生类似的产品,工厂模式只需要我们传入正确的参数,就能生产类似的产品;
-
工厂模式根据抽象程度依次分为简单工厂模式、工厂方法模式、抽象工厂模式;
2. 实现
1. 简单工厂模式:
在我们的生活中很多时候就有这样的场景,像在网站中有的页面是需要根据账号等级来决定是否有浏览权限的;账号等级越高可浏览的就越多,反之就越少;
// JS设计模式之简单工厂
function factory(role){
function superAdmin(){
this.name="超级管理员";
this.viewPage=["首页","发现页","通讯录","应用数据","权限管理"];
}
function admin(){
this.name="管理员";
this.viewPage=["首页","发现页","通讯录","应用数据"];
}
function user(){
this.name="普通用户";
this.viewPage=["首页","发现页","通讯录"];
}
switch(role){
case "superAdmin":
return new superAdmin();
break;
case "admin":
return new admin();
break;
case "user":
return new user();
break;
}
}
let superAdmin = factory("superAdmin");
console.log(superAdmin);
let admin = factory("admin");
console.log(admin);
let user = factory("user");
console.log(user);
上述代码中,factory就是一个工厂,factory有三个函数分别是对应不同的产品,switch中有三个选项,这三个选项相当于三个模具,当匹配到其中的模具之后,将会new一个构造函数去执行生产工厂中的function;但是我们发现上面的简单工厂模式会有一定的局限性,就是如果我们需要去添加新的产品的时候,我们需要去修改两处位置(需要修改function和switch)才能达到添加新产品的目的;
下面我们将简单工厂模式进行改良:
// JS设计模式之简单工厂改良版
function factory(role){
function user(opt){
this.name = opt.name;
this.viewPage = opt.viewPage;
}
switch(role){
case "superAdmin":
return new user({name:"superAdmin",viewPage:["首页","发现页","通讯录","应用数据","权限管理"]});
break;
case "admin":
return new user({name:"admin",viewPage:["首页","发现页","通讯录","应用数据"]});
break;
case "normal":
return new user({name:"normal",viewPage:["首页","发现页","通讯录"]});
}
}
let superAdmin = factory("superAdmin");
console.log(superAdmin);
let admin = factory("admin");
console.log(admin);
let normal = factory("normal");
console.log(normal);
2. 工厂方法模式:
工厂方法模式是将创建对象的工作推到子类中进行;也就是相当于工厂总部不生产产品了,交给下辖分工厂进行生产;但是进入工厂之前,需要有个判断来验证你要生产的东西是否是属于我们工厂所生产范围,如果是,就丢给下辖工厂来进行生产,如果不行,那么要么新建工厂生产要么就生产不了;
// JS设计模式之工厂方法模式
function factory(role){
if(this instanceof factory){
var a = new this[role]();
return a;
}else{
return new factory(role);
}
}
factory.prototype={
"superAdmin":function(){
this.name="超级管理员";
this.viewPage=["首页","发现页","通讯录","应用数据","权限管理"];
},
"admin":function(){
this.name="管理员";
this.viewPage=["首页","发现页","通讯录","应用数据"];
},
"user":function(){
this.name="普通用户";
this.viewPage=["首页","发现页","通讯录"];
}
}
let superAdmin = factory("superAdmin");
console.log(superAdmin);
let admin = factory("admin");
console.log(admin);
let user = factory("user");
console.log(user);
工厂方法模式关键核心代码就是工厂里面的判断this是否属于工厂,也就是做了分支判断,这个工厂只做我能生产的产品,如果你的产品我目前做不了,请找其他工厂代加工;
3. 抽象工厂模式:
如果说上面的简单工厂和工厂方法模式的工作是生产产品,那么抽象工厂模式的工作就是生产工厂的;
举个例子:代理商找工厂进行合作,但是工厂没有实际加工能力来进行代加工某产品;无奈又签署了合同,这时,工厂上面的集团公司就出面了,集团公司承认该工厂是该集团下属公司,所以集团公司就重新建造一个工厂来进行代加工某商品以达到履行合约;
//JS设计模式之抽象工厂模式
let agency = function(subType, superType) {
//判断抽象工厂中是否有该抽象类
if(typeof agency[superType] === 'function') {
function F() {};
//继承父类属性和方法
F.prototype = new agency[superType] ();
console.log(F.prototype);
//将子类的constructor指向子类
subType.constructor = subType;
//子类原型继承父类
subType.prototype = new F();
} else {
throw new Error('抽象类不存在!')
}
}
//鼠标抽象类
agency.mouseShop = function() {
this.type = '鼠标';
}
agency.mouseShop.prototype = {
getName: function(name) {
// return new Error('抽象方法不能调用');
return this.name;
}
}
//键盘抽象类
agency.KeyboardShop = function() {
this.type = '键盘';
}
agency.KeyboardShop.prototype = {
getName: function(name) {
// return new Error('抽象方法不能调用');
return this.name;
}
}
//普通鼠标子类
function mouse(name) {
this.name = name;
this.item = "买我,我线长,玩游戏贼溜"
}
//抽象工厂实现鼠标类的继承
agency(mouse, 'mouseShop');
//子类中重写抽象方法
// mouse.prototype.getName = function() {
// return this.name;
// }
//普通键盘子类
function Keyboard(name) {
this.name = name;
this.item = "行,你买它吧,没键盘看你咋玩";
}
//抽象工厂实现键盘类的继承
agency(Keyboard, 'KeyboardShop');
//子类中重写抽象方法
// Keyboard.prototype.getName = function() {
// return this.name;
// }
//实例化鼠标
let mouseA = new mouse('联想');
console.log(mouseA.getName(), mouseA.type,mouseA.item); //联想 鼠标
//实例化键盘
let KeyboardA = new Keyboard('联想');
console.log(KeyboardA.getName(), KeyboardA.type,KeyboardA.item); //联想 键盘
抽象工厂模式一般用于严格要求以面向对象思想进行开发的超大型项目中,我们一般常规的开发的话一般就是简单工厂和工厂方法模式会用的比较多一些;
大白话解释:简单工厂模式就是你给工厂什么,工厂就给你生产什么;
工厂方法模式就是你找工厂生产产品,工厂是外包给下级分工厂来代加工,需要先评估一下能不能代加工;能做就接,不能做就找其他工厂;
抽象工厂模式就是工厂接了某项产品订单但是做不了,上级集团公司新建一个工厂来专门代加工某项产品;
2. 设计原则:
有哪些设计原则:
根据英文首单词我们又称为S.O.L.I.D.设计原则,一共有5种设计原则,根据分类分别为:
1、S(Single responsibility principle)——单一职责原则
一个程序或一个类或一个方法只做好一件事,如果功能过于复杂,我们就拆分开,每个方法保持独立,减少耦合度;
2、O(Open Closed Principle)——开放封闭原则
对扩展开放,对修改封闭;增加新需求的时候,我们需要做的是增加新代码,而非去修改源码;
例如:我们在使用vue框架的时候,有很多第三方插件我们可以去使用,在使用的时候我们通常都是直接在vue-cli中增加引入代码,而非去修改vue源码来达到支持某种功能的目的;
3、L(Liskov Substitution Principle, LSP)——李氏置换原则
子类能覆盖父类,父类能出现的地方子类就能出现;(在JS中没有类概念,使用较少)
4、I (Interface Segregation Principle)——接口独立原则
保持接口的单一独立,类似于单一原则,不过接口独立原则更注重接口;(在JS中没有接口概念)
5、D(Dependence Inversion Principle ,DIP)——依赖倒置原则
面向接口编程,依赖于抽象而不依赖于具体,使用方只关注接口而不需要关注具体的实现;(JS中没有接口概念)
作为前端开发人员来说,我们用的最多的设计原则是S(单一职责原则).O(开放封闭原则),所以在程序设计的时候我们重点关注这两个即可 ;
通常在做很多事情的时候,都会有一定的规范制约;在软件开发的过程中,我们可以将设计原则视为一种开发规范,但不是必须要遵循的,只是不遵循的话,代码后期的维护和复用都会变得很糟糕;
遵循设计原则可以帮助我们写出高内聚、低耦合的代码,当然代码的复用性、健壮性、可维护性也会更好;