目录
28、设计模式
设计模式(Design Pattern)是前辈们对代码开发经验的总结,是解决特定问题的一系列套路。它不是
语法规定,而是一套用来提高代码可复用性、可维护性、可读性、稳健性以及安全性的解决方案。
1995 年,GoF(Gang of Four,四人组/四人帮)合作出版了《设计模式:可复用面向对象软件的基
础》一书,共收录了 23 种设计模式,从此树立了软件设计模式领域的里程碑,人称「GoF设计模式」。
这些模式可以分为三大类:创建型模式(Creational Patterns)、结构型模式(Structural
Patterns)、行为型模式(Behavioral Patterns)。
创建型模式
工厂模式(Factory Pattern)提供了一种创建对象的最佳方式
抽象工厂模式(Abstract Factory Pattern)
单例模式(Singleton Pattern)系统中被唯一使用,一个类只有一个实例
建造者模式(Builder Pattern)
原型模式(Prototype Pattern)
结构型模式
适配器模式(Adapter Pattern)将原本不适合的接口转换为适合的接口
桥接模式(Bridge Pattern)
过滤器模式(Filter、Criteria Pattern)
组合模式(Composite Pattern)
装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构
外观模式(Facade Pattern)
享元模式(Flyweight Pattern)
代理模式(Proxy Pattern)代替被请求者来处理相关事务
行为型模式
责任链模式(Chain of Responsibility Pattern)
命令模式(Command Pattern)
解释器模式(Interpreter Pattern)
迭代器模式(Iterator Pattern)
中介者模式(Mediator Pattern)
备忘录模式(Memento Pattern)
观察者模式(Observer Pattern)当一个对象数据发生变化时,通知其它一系列对象,让其响应这种变化
状态模式(State Pattern)
空对象模式(Null Object Pattern)
策略模式(Strategy Pattern)
模板模式(Template Pattern)
访问者模式(Visitor Pattern)
计模式的六大原则
1.
开闭原则(Open Close Principle)
意思是:对扩展开放,对修改关闭。
2.
里氏代换原则(Liskov Substitution Principle)
里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。
3.
依赖倒转原则(Dependence Inversion Principle)
具体内容:针对接口编程,依赖于抽象而不依赖于具体
4.
接口隔离原则(Interface Segregation Principle)
使用多个隔离的接口,比使用单个接口要好,降低类之间的耦合度。。
5.
迪米特法则,又称最少知道原则(Demeter Principle)
一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立。
6.
合成复用原则(Composite Reuse Principle)
尽量使用合成/聚合的方式,而不是使用继承。
其实设计模式就是从大型软件架构出发、便于升级和维护的软件设计思想,它强调降低依赖,降低耦
合。
(1)观察者模式
当一个对象数据发生变化时,通知其它一系列对象,让其响应这种变化
观察者设计模式中主要区分两个概念:
观察者
:指观察者对象,也就是消息的订阅者;
被观察者:
指要观察的目标对象,也就是消息的发布者。
// vue核心思路===观察者模式
// 响应式属性
// 场景:订外卖 对象:商家(发布者) 商品(订阅的内容) 小哥 客户(订阅者)
class Subject { //发布者
constructor() {
this.subs = []; //订阅列表
}
addSub(sub) { //添加订阅者
this.subs.push(sub);
}
// 当商品ready去通知用户
notify(food) { //遍历所有的订阅者
this.subs.forEach(sub => {
sub.update(food);
});
}
}
class Observer { //订阅者--客户
constructor(name, food) {
this.name = name;
this.food = food;
}
update(food) {
if (food === this.food) {
console.log(this.name + "的外卖:" + food);
}
}
}
var subject = new Subject();
var tom = new Observer("tom", "地三鲜");
var jack = new Observer("jack", "红烧肉");
//目标添加观察者了
subject.addSub(tom);
subject.addSub(jack);
//目标发布消息调用观察者的更新方法了
subject.notify("地三鲜");
subject.notify("红烧肉");
优点:
1. 观察者模式在被观察者和观察者之间建立一个抽象的耦合。被观察者角色所知道的只是一个具体观 察者列表,每一个具体观察者都符合一个抽象观察者的接口。被观察者并不认识任何一个具体观察 者,它只知道它们都有一个共同的接口。由于被观察者和观察者没有紧密地耦合在一起,因此它们 可以属于不同的抽象化层次。如果被观察者和观察者都被扔到一起,那么这个对象必然跨越抽象化 和具体化层次。
2. 观察者模式支持广播通讯。被观察者会向所有的登记过的观察者发出通知。
(2)代理模式
代理是一个对象,跟本体对象具有相同的接口,以此达到对本体对象的访问控制。 代理,顾名思义,即代替被请求者来处理相关事务。代理对象一般会全权代理被请求者的全部只能,客 户访问代理对象就像在访问被请求者一样,虽然代理对象最终还是可能会访问被请求者,但是其可以在 请求之前或者请求之后进行一些额外的工作,或者说客户的请求不合法,直接拒绝客户的请求。
// 声明女孩对象
var girl = function (name) {
this.name = name;
};
// 声明男孩对象
var boy = function (girl) {
this.girl = girl;
this.sendGift = function (gift) {
alert("Hi " + girl.name + ", 男孩送你一个礼物:" + gift);
}
};
// 声明代理对象
var proxyObj = function (girl) {
this.girl = girl;
this.sendGift = function (gift) {
(new boy(girl)).sendGift(gift); // 替dudu送花咯
}
};
var proxy = new proxyObj(new girl("花花"));
proxy.sendGift("999朵玫瑰");
优点:
代理对象可以代替本体对象被实例化,此时本体对象未真正实例化,等到合适时机再实例化。
代理模式可以延迟创建开销很大的本体对象,他会把本体的实例化推迟到有方法被调用时。
(3)工厂模式
最常用的设计模式之一,这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
// function
var MobileFactory = (function () {
var Mobile = function (name, model) {
this.model = model;
this.name = name;
};
Mobile.prototype.play = function () {
console.log("Mobile:" + this.name + "-" + this.model)
}
return function (name, model) {
return new Mobile(name, model);
};
})();
============================== 上面主要针对这里 =====================================
//创建对象简单
var p6 = new MobileFactory("iphone", "6");
var px = new MobileFactory("iphone", "X");
p6.play()
px.play()
===================================================================================
//Creator是个工厂,有个create函数,工厂通过create创建Product
class Product {
constructor(name, model) {
this.name = name
this.model = model;
}
play() {
console.log("Mobile:" + this.name + "-" + this.model)
}
}
//主要是这里可以写if在函数内,用来限制部分对象
class FactoryCreator {
create(name, model) {
return new Product(name, model)
}
}
================================== 工厂模式 ========================================
let creator = new FactoryCreator()
// 通过工厂省城product的实例
let p = creator.create('iphone', "6")
p.play();
let p2 = creator.create('iphone', "7")
p2.play()
===================================================================================
优点
1. 工厂类含有必要的判断逻辑,可以决定在什么时候创建哪一个产品类的实例,客户端可以免除直接 创建产品对象的责任,而仅仅”消费”产品。工厂模式通过这种做法实现了对责任的分割。
2. 当产品有复杂的多层等级结构时,工厂类只有自己,以不变应万变,就是模式的缺点。因为工厂类 集中了所有产品创建逻辑,一旦不能正常工作,整个系统都要受到影响。
缺点
1. 系统扩展困难,一旦添加新产品就不得不修改工厂逻辑,有可能造成工厂逻辑过于复杂, 违背了”开 放–封闭”原则(OCP) .
(4)单例模式
系统中被唯一使用,一个类只有一个实例
class Store {
action() {
console.log('vue store.')
}
}
// 定义一个静态的方法,将方法挂载到class上面,无论SingleObject被new多少个,getInstance的方法只有一个
Store.getInstance = (function () {
let instance
return function () {
if (!instance) {
instance = new Store();
}
return instance
}
})()
// js中的单例模式只能靠文档去约束,不能使用private关键字去约束
// 测试:注意这里只能使用静态函数getInstance,不能使用new Store()
let obj1 = Store.getInstance()
obj1.action()
let obj2 = Store.getInstance()
obj2.action()
// 单例模式(唯一的),每次获取的都是一个东西,所以他两相等,否则就不是单例模式
console.log(obj1 === obj2) //true
//无论new多少次都是同一个对象
优点:
1. 在单例模式中,活动的单例只有一个实例,对单例类的所有实例化得到的都是相同的一个实例。这 样就防止其它对象对自己的实例化,确保所有的对象都访问一个实例
2. 避免对共享资源的多重占用。
(5)适配器模式
将原本不适合的数据转换为适合的数据(计算属性)。
将原本不适合的接口转换为适合的接口。
// 适配器模式--插座
// 只有2口插座---现在要接3口的?---要买一个插版(适配器),即有2口,也有3口
// 站在框架的角度vue---提供给用户使用---响应式+计算属性+watch
class IPhoneX {
constructor() {
this.interface = "平接口type-c";
}
}
// 适配器对象 提供了旧接口 和 新接口
class Adapter {
// 旧接口, 新接口 把旧的接口适配成新的接口
constructor(oldInter, newInter) {
this.phone = new IPhoneX() // 初始化实例
this.oldInter = oldInter
this.newInter = newInter
}
// 可以使用旧接口
getOldInter() {
return this.oldInter
}
translate(p, newInter) { //模拟转接头
console.log(`${p.interface}---->>${newInter}`)
}
// 使用新接口
getInter() { // 覆盖
this.translate(this.phone, this.newInter);
return this.newInter
}
}
let adapter = new Adapter('圆接口', '平接口')
let res = adapter.getInter()
console.log(res) // 圆接口
res = adapter.getOldInter()
console.log(res) // 平接口
优点:
1. 将目标类和适配者类解耦.
2. 增加了类的透明性和复用性,将具体的实现封装在适配者类中.
(6)装饰器模式
允许向一个现有的对象添加新的功能,同时又不改变其结构。
比喻:
假如我有一个蛋糕,如果在上面加上奶油其他什么都不加,那么它就成了一个奶油蛋糕。如果再加上草 莓,那就是草莓奶油蛋糕,如果再加上一块巧克力板,上面写上姓名,然后插上蜡烛,就变成了一块生 日蛋糕。
不论是蛋糕,奶油蛋糕,草莓蛋糕还是生日蛋糕,它们的核心都是蛋糕。不过,在加上一系列装饰之 后,它变得更加甜美了,目的也更加明确了。装饰器模式中的被装饰对象和蛋糕很相似。
// 装饰器----人靠衣妆
// 框架vue: v-on@click @click @click.native
class Circle {
draw() {
console.log('我要画一个圆')
}
}
class Decorator {
constructor(circle) {
this.circle = circle
}
draw() {
this.circle.draw()
this.setBorder(circle)
}
setBorder(circle) {
console.log('加上边框')
}
}
// 想引用某个类的时候将他实例化,以参数的形式进行传递进去
let circle = new Circle()
circle.draw()
console.log('+++++')
let dec = new Decorator(circle)
dec.draw()
/
最后一个就是装饰的
优点:装饰类和被装饰类可以独立发展,不会相互耦合,装饰模式可以动态扩展一个实现类的功能。
装饰器模式和代理模式的区别:
装饰器模式关注于在一个对象上动态的添加方法,然而代理模式关注于控制对对象的访问
。换句话说,
用代理模式,代理类(proxy class)可以对它的客户隐藏一个对象的具体信息。因此,当使用代理模式
的时候,我们常常在一个代理类中创建一个对象的实例。并且,当我们使用装饰器模式的时候,我们通
常的做法是
将原始对象作为一个参数传给装饰者的构造器
。
装饰器模式与适配器模式的区别
装饰器与适配器都有一个别名叫做
包装模式(Wrapper)
,它们看似都是起到包装一个类或对象的作用,
但是使用它们的目的很不一一样。
适配器模式的意义是要将一个接口转变成另一个接口,它的目的是通
过改变接口来达到重复使用的目的。
而装饰器模式不是要改变被装饰对象的接口
,而是恰恰要保持原有的接口,但是增强原有对象的功能,
或者改变原有对象的处理方式而提升性能。所以这两个模式设计的目的是不同的。
29、设计模式分析
依赖具体:
老板要造一辆小车,找最好的工匠制作最好的零部件,如轮胎,底盘等,由于长时间使用一些零件开始损坏,如轮胎破了需要再做新的,再找工匠对比之前的轮胎制作。
依赖抽象:
老板要造一辆小车,先找最好的设计人员设计出详细的尺寸,规格等,并找工匠实现出原型产品,当原 型产品损坏后,可以再照着设计图再制作轮胎,自己制作还可以扩展,如加上自己的签名,特别的花纹等。
30、组件封装
封账就是隐藏实现细节,仅对外提供访问的接口。内部的具体实现细节不关心。
(1)封装好处
模块化,分工明确
信息隐藏
代码重用
(2)HttpAjax组件封装
// 封装原生XMLHttpRequest请求
function createXMLHttp(baseUrl) {
// 低版本IE兼容
var XmlHttp = window.XMLHttpRequest ? new XMLHttpRequest() :
ActiveXobject("Msxml12.XMLHTTP");
// 默认请求 协议、IP、port
var defaultURL = baseUrl ? baseUrl : "http://localhost:3000";
return {
sendRequest({ method = 'GET', url, data = null, success, error }) {
XmlHttp.open(method, defaultURL + url);
if (method == "GET") {
XmlHttp.send();
} else {
XmlHttp.setRequestHeader("Content-Type", "application/json")
XmlHttp.send(data);
}
XmlHttp.onreadystatechange = function (resp) {
if (XmlHttp.readyState == 4 && XmlHttp.status == 200) {
success(resp);
}
};
XmlHttp.onerror = function (err) {
error(err);
}
}
}
}
31、ES6
(1)简介
ES6
是 JS 语言的下一代标准
,已经在 2015 年 6 月正式发布了。
它的目标,是使得 JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言。
(2)Babel 转码器
Babel是一个广泛使用的 ES6 转码器,可以将 ES6 代码转为 ES5 代码,从而在老版本的浏览器执行。这意味着,你可以用 ES6 的方式编写程序,又不用担心现有环境是否支持。下面是一个例子。
(3)let与const
1.let的定义
ES6 新增了 let 命令,用来声明变量,它的用法类似于 var
2.let 和var的区别
(1)不存在变量提升
console.log(a); //undefined
var a = 'a';
console.log(b); //报错
let b = 'b';
(2)同一个作用域内不能重复定义同一个名称
var a = 10;
var a = 999;
console.log(a);
let b = 1;
let b = 2;
console.log(b); //报错,不能重复定义
//let不能在同一作用域下定义
//
唯一赋值方式
(3)有着严格的作用域
var
函数作用域、
let
块级作用域
(4)块级作用域的重要性
var
函数作用域、
let
块级作用域
//
因为:let当遍历到指定的值之后,就不会执行
//
结果为最后一个
//
结果未指定数值
var
函数作用域、
let
块级作用域
3.暂时性死区
因为let没有预解析?
(1)坑1:
//相当于定义了一个var i = 100
(2)坑2:
- 坑3:
//
因为b不存在
4.const 的定义
const
声明一个只读的常量。一旦声明,常量的值就不能改变。
保持let的不存在变量提升,同一个作用域内不能重复定义同一个名称,有着严格的作用域;
(1)修改常量的值会报错
//修改常量的值会报错
const x = 12345;
x = 999; //error
//const声明的变量不得改变值,这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋值。
===================================================================================
const y; //error
//只声明不赋值,也会报错
===================================================================================
var a = 10;
let b = 100;
const a = 999; //error
const b = 9999; //error
(2)const的坑
const
实际上保证的,并不是变量的值不得改动,
而是变量指向的那个内存地址所保存的数据不得改动。
对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同 于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实 际数据的指针, const 只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数 据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。
//常量obj储存的是一个地址,这个地址指向一个对象。不可变的只是这个地址,即不能把obj指向另一个地址,
//但对象本身是可变的,所以依然可以为其添加新属性。
const obj = {};
obj.name = "amy";
obj['age'] = 18;
console.log(obj);
obj = { id: 1 }; //报错(不能把obj指向另一个地址)
//===================================================================================
//常量arr是一个数组,这个数组本身是可写的,但是如果将另一个数组赋值给arr,就会报错。
const arr = [];
arr.push(1);
arr[1] = 2;
arr = ['a'] //但是如果将另一个数组赋值给arr,就会报错
(4)解构
1.什么是解构
ES5中的为变量赋值,只能直接指定值
var a = 1;
var b = 2;
var c = 3;
2.
数组
解构
//如果另一个
[ ]
解构内未赋值,则默认使用
let
、
var[ ]
内的值
(1)es6匹配模式写法
//这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值
var [a,b,c] = [1,2,3];
等同于 ==
//1
,2,4
let [a] = [];
let [a,b] = [1]; //为undefined(匹配不成功)
(2)解构赋值允许指定默认值
var [x = 1,y = 2] = [10,20]; // x = 10 ,y = 20;
var [x = 1,y = 2] = [10]; // x = 10 ,y = 2;
var [x = 10,y = 20] = []; //x=10,y=20;
var [x,y = 'b']= ['a']; //x='a',y='b';
var [x = 1,y] = [10,20]; //x = 10,y=20;
var [x = 1] = [null]; //x = null
只有当一个数组成员严格等于undefined,默认值才会生效
var [x = 1]=[undefined]; //x=1;
var [x,y = 'b'] =['a',undefined]; //x='a',y='b';
(3)惰性求值
如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值
function f() {
return '12345'
}
var [x = f()] = [1]; //x=1
var [x = f()] = []; //x='12345'
var [x = f()] = [undefined]; //x='12345'
3.
对象
解构
对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。
(1)对象解构的写法
//对象解构的写法:
let {a,b} = {b:1,a:2};
(2)实际的运用
var res = {
code: 200,
msg: 'success',
result: [1, 2, 3, 4],
obj: {id: 1},
arr: [{name: 1}]
};
//ES5
//var code = res.code;
//var result = res.result;
//var obj = res.obj;
//ES6
let {code, obj, result} = res;
result[2];
res.id;
//扩展
function fun(str, {code, obj}) {
console.log(code)
};
fun(1, res) //200
(3)别名
变量名与属性名不一致,
name
是匹配模式,
n
是变量(n、a是别名)
//console.log
(
name
)会报错
(4)默认值
下面第三个对象的属性值等于undefined,默认值生效
let {x,y = 5} = {x:10}; //x=10,y=5
let {x:y = 5} ={y:50}; //y=50
let {x:a = 10,y:b=20} = {x:undefined}; //a=10,b=20
(5)注意
如果要将一个已经声明的变量用于解构赋值,必须非常小心。
//错误的写法
let x;
{x} = {x: 1};
//报错的原因:因为 JavaScript 引擎会将{x}理解成一个代码块,从而发生语法错误。只有不将大括号写在行首,避免 JavaScript 将其解释为代码块,才能解决这个问题
//正确的写法
let x;
({x} = {x: 1});
//将整个解构赋值语句,放在一个圆括号里面,就可以正确执行
4.字符串解构
let [a,b,c,d,e,f] ='nodejs';
5.函数参数解构
解构在函数中具体运用
//数组解构
function add([x, y]) {
return x + y;
}
add([1, 2]); //3
add([, 2]); //NaN
//对象解构
function move({x, y}) {
return x + y;
}
move({x: 3, y: 8}); //11
参数默认值的定义:
function move({x = 0, y = 0}) {
return x + y;
}
move({ x: 3, y: 5 }); //8
===================================================================================
function fun([x = 0, y = 0]) {
return x + y;
};
fun([2]); //2
===================================================================================
function fun([x = 0, y = 0]) {
return x + y;
};
fun([, 4]); //4
===================================================================================
function move({x = 0, y = 0}) {
return x + y;
}
move({x: 3}); //3
===================================================================================
function move({ x = 0, y = 0 }) {
return x + y;
}
move({y: 5}); //5
扩展题:
function move({x = 0, y = 0} = {}) {
return [x, y];
}
move({x: 3, y: 8}); //[3,8]
move({x: 3}); //[3,0]
move({}); //[0,0]
move(); //[0,0]
===================================================================================
function move({x, y} = {x: 0, y: 0}) {
return [x, y];
}
move({x: 3, y: 8}); //[3,8]
move({x: 3}); //[3,undefined]
move({}); //[undefined,undefined]
move(); //[0,0] 默认值