前言
- 面试的时候面试官问你了解设计模式多少?
- 而我却含含糊糊、吞吞吐吐说了些面试官并不想听到的答案
- 所以为了不在面试的时候尴尬,小编带领大家了解一下JS设计模式,让大家在面试的时候胸有成竹!
- 建议大家先收藏,有空的时候look一look,字有点长,笔芯~
27个方面
1. 搭建开发环境
- 因为这里想在编辑器上更好的演示设计模式,所以这里简单的搭下环境(webpack、babel(为了让es6转为es5))。
- 初始化:
npm init
- webpack安装:
npm i webpack webpack-cli --save-dev
- webpack-dev-server和html-webpack-plugin安装:
npm install webpack-dev-server html-webpack-plugin --save-dev (webpack不会实时更新,而webpack-dev-server会实时更新; html-webpack-plugin网页模板的插件)
- babel(es6 -> es5):
npm install babel-core babel-loader babel-polyfill babel-preset-es2015 babel-preset-latest --save-dev
- 此时可以npm run dev,但可能npm会提示需要再安装babel-loader,如果遇到就:
npm install babel-loader@7
- webpack.dev.config配置
- 添加之后,开源npm run dev调试
- es6转为es5
2. 了解面向对象
- 对象 == 类(模板),类中有属性和方法
- 如何生成对象? 通过new对象(实例)
- 面向对象三要素:封装继承多态
封装:三要素public、private、protected(对子类开放),目前es6不支持,一般认为_开头的属性是private
多态:父类的方法不实现,让子类按自己的需求自己去实现 为何使用面向对象?
1. 程序执行:顺序、判断、循环 --- 结构化
2. 面向对象 --- 数据结构化
3. 对于计算机,结构化的才是最简单的
4. 变成应该 简单 & 抽象
3.设计原则
- 使用设计模式,要画UML图,这里介绍两种画图工具:
需要下载的软件:ms office visio
在线制作:www.processon.com/diagrams
画图规范可以按照这个模板写类Class 先画图,再写代码
说到设计原则,这里必须提下《unix/linux设计哲学》这本书,告诉我们什么是设计
1. 舍弃高效性而取可移植性:写好一个东西可以复制去别的地方用,比,一个东西效能速度好,但是移植性差,因为老是要改,所以移植性比较重要
2. 采用纯文本存数据
3. 允许用户去定制环境(环境不能写死,像那些配置背景什么的)
4. 沉默是金:返回是数字,如果返回个“这不是数字”字符串就不行,因为人家就是要返回number,是字符串就不要返回,返回个-1也行
5. 需求90%的解决方案:花20%的成本解决80%的问题,如果就要解决剩下的20%的问题,就要花很大力气去解决,没必要,能满足大部分人的需求就好,没必要做到完美Solid五大设计原则(刚好是css中是实线的意思)
1. S - 单一职责原则(一个程序做好自己就好,不要为了另外一个方法实现,加很多东西,变得不复用;如果功能过于复杂就拆开,每个部分保持独立,不要混合,要小而精)
2. O - 开发封闭原则(对扩展开放,对修改关闭;增加需求时,扩展新代码,而非修改已有代码 )
3. L - 李氏置换原则(父类能出现的地方,子类都能出现,因为子类继承他的,爸爸有的,儿子肯定也有)
4. I - 接口独立原则(接口保证独立开发)
5. D - 依赖导致原则(编程要依赖抽象,不要依赖实现;关注怎么调用接口,不关注接口是怎么实现的)
用Promise来说明SO
分析:这里第一个then做好自己的,第二个then也是,没有在第一个then里面加上第二个then的实现,每个then的逻辑只做好一件事情,体现S(单一原则);
增加需求,也就是第二个then,没有在第一个then里面改,而是添加一个新的then,如果新增需求,扩展then,体现O(开放封闭原则);
顺带一提:ajax就是全部东西都混在一起,只有一个成功函数ssucess,违反SO原则result.then(function(img){ console.log('img.width', img.width) return img}).then(function(img){ console.log('img.height', img.height) }).cathc(function(ex){ //统一捕获异常 console.log(ex) })
4.理解设计模式?
分开看,先“设计”,在“模式”
5.如何学习设计模式?
刻意练习
下面有道面试题:请画出UML图
//面试题停车场停车显示//显示屏class Screen{show(car,inTime){console.log('车牌号',car.num)console.log('停车时间',Date.now() - inTime)}}//摄像头class Camera{shot(car){return {num:car.num,inTime:Date.now()}}}//停车场class Park{constructor(floors){this.floor = floor || [];this.camera = new Camera()this.screen = new Screen()this.carList = {} //存摄像头拍摄的车辆信息}in(car){// 通过摄像头获取信息const info = this.camera.shot(car)//停到某个停车位const i = parseInt(Math.random() * 100 % 100)const place = this.floor[0].place[i]place.in()info.place = place//记录信息this.carList[car.num] = info}out(car){//获取信息const info = this.carList[car.num]//将停车位清空const place = info.placeplace.out()//显示时间this.screen.show(car,info.inTime)//清记录,不然造成内存泄漏delete this.carList[car.num]}emptyNum(){return this.floor.map( floor=>{return `${floor.index}层还有${floor.emptyPlaceNum()}车位`}).join('\n')}}//层class Floor{constructor(index,place){this.index = indexthis.place = place || [];}emptyPlaceNum(){let num = 0;this.place.forEach(p =>{if(p.empty){num += 1}})return num}}//车位class Place{constructor(){this.empty = true}in(){this.empty = false}out(){this.empty = true}}//车class Car{constructor(num){this.num = num}}//测试//初始化停车场var floor = []for(var i = 0;i<3;i++){var place = []for(var j=0;j<100;j++){place[j] = new Place()}floor[i] = new Floor(i+1,place)}const park = new Park(floor)//初始化车辆const car1 = new Car(100)const car2 = new Car(200)console.log('car1进入')console.log(park.emptyNum())park.in(car1)console.log('car2进入')console.log(park.emptyNum())park.in(car2)console.log(park.emptyNum())console.log('car2离开')park.out(car2)console.log(park.emptyNum())
6.工厂模式(有new实例就考虑工厂模式,其实工厂模式就是为了省去new操作)
- UML图
- 代码演示
//产品class Product{constructor(name){this.name = name}init(name){console.log('init'+name)}fn1(){console.log('fn1')}fn2(){console.log('fn2')}}//工厂class Creator{create(name){return new Product(name)}}var creator = new Creator()var product = creator.create('pppp')product.init()//可以这样封装Window.creatorCreate = function(name){return new Creator(name)}//外部使用就Window.creatorCreate().create("ha").fn1()//其实就是省去new这个操作//如:$('div')就是工厂模式,如果不是的话就是 new $('div')
- 场景:jQuery
- 场景2:React.createElements
class Vnode(tag, attrs, chilren){
// ...省略内部代码...
}
React.createElements = function(tag, attrs, children){
return new Vnode(tag, attrs, chilren)
}
7.单例模式(系统中被唯一使用;一个类只有一个实例)
- Java通过private私有化实现单例模式
- java演示单例模式
代码演示
class SingleObject{login(){console.log("login。。。。")}}SingleObject.getInstance = (function(){//通过闭包let instance;return function(){if(!instance){instance = new SingleObject()}return instance}})()let obj1 = SingleObject.getInstance()obj1.login()let obj2 = SingleObject.getInstance()obj2.login()console.log(obj1===obj2) //true//看这个相等,因为两个都是通过getInstance方法,//第一个obj1已经new了,所以第二个不走if,直接return,所以才二者相等//如果通过new出来的,则是不相等的let ob3 = new SingleObject()ob3.login()console.log(obj1===ob3) //false
场景:像登录框一样,整个系统只会用到一次
- 场景2:jQuery本来就是单例模式
- 场景3:模拟登录框,若已有弹框,再有代码让他弹框,是不会弹的,因为系统弹框状态一样
- 代码演示:
// 举个例子:模拟登录框class LoginForm{constructor(){this.state = 'hide'}show(){if(this.state === 'show'){alert('已显示')return}this.state = 'show'console.log('登录框已经显示')}hide(){if(this.state === 'hide'){alert('已隐藏')return}this.state = 'hide'console.log('登录框已经隐藏')}}LoginForm.getInstance = (function(){let instance1return function(){if(!instance1){instance1 = new LoginForm()}return instance1}})()let login1 = LoginForm.getInstance();login1.show()let login2 = LoginForm.getInstance();login2.hide()login2.hide()//登录框如果是隐藏,在写隐藏,就会弹出已隐藏了,//也就是他们的状态整个系统都是共用的,这就是单例模式//好比vue的store,达到数据可以随意拿,就是使用单例模式
- 场景4:和vue中的store数据共享,因为是单例模式,所以他们的store都是一样的
8.适配器模式(旧接口格式和使用者不兼容;中间加个适配器转换接口) 旧接口不能用了,把旧的功能放在新的身上
- UML图
- 代码演示:
class Adaptee{specificRequest(){return '德国插头'}}class Target{constructor(){this.adaptee = new Adaptee()}request(){let info = this.adaptee.specificRequest()return `${info} ->转换器 -》 中国标准插头`}}//测试let target = new Target()console.log(target.request())//其实就是把那个不能用的方法,在new一个实例,丢进去使用
- 场景1:jQuery的ajax,形式是$.ajax({}),我现在想用我自己写的$.ajax,使用适配器,现在使用$.ajax就直接调用我们自己封装写的ajax
- 封装旧接口
// 自己封装的ajax,使用方式如下: ajax({ url:'/getData', type:'post', dataType:'json', data:{ id:"123" }}) .done(function(){}) //但因为历史原因,代码全都是: //$.ajax({...}) 解决方案:封装旧接口 var $ = { ajax:function(option){ return ajax(option) //这个ajax是我们自己定义的 }}
- 场景2:通过js的方法对字符串进行一系列操作(反转,切割)
- 先补习下知识:重新开个端口运行html
- 安装:npm install http-server -g,可以重新开个端口显示另外一个页面
- 使用:http-server -p 8881
- 代码演示:
<div id="app"><p>顺序:{{message}}</p><p>逆序:{{reveredMessage}}</p></div><script src="https://cdn.bootcss.com/vue/2.6.11/vue.js"></script><script>var vm = new Vue({el:"#app",data:{message:'hello'},computed:{reveredMessage:function(){//相当于适配器,拿到以前的message,然后转换使用return this.message.split("").reverse().join("")}}})</script>
9.装饰器模式(为对象添加新功能,不改变其原有的结构功能)
注:和适配器模式不一样,适配器是旧的接口不能用,装饰器模式是旧的接口还可以用,在之上添加功能
- 代码演示
class Circle{draw(){console.log('画一个圆')}}class Decorator{constructor(circle){this.circle = circle}draw(){this.circle.draw()this.setRedBorder(circle)}setRedBorder(circle){console.log('设置红色边款')}}//测试var circle = new Circle()circle.draw()console.log('----')var dec = new Decorator(circle)dec.draw() // 还是原有的方法,但是多了个setRedBorder方法
- 场景1:es7装饰器
- 安装es7环境插件:npm install babel-plugin-transform-decorators-legacy --save-dev
- 代码演示:
//使用场景 es7装饰器,对class进行装饰而已//可传参@testDec(false)class Demo{}function testDec(isDec){return function(target){target.isDec = isDec}}console.log(Demo.isDec)//下一个例子function mixins(...list){return function(target){//将传进来的list参数和target的原型上合并,多了个list的方法或属性Object.assign(target.prototype,...list)}}const Foo = {foo(){console.log('foo')}}//装饰器也可以传方法,装饰类@mixins(Foo)class MyClass{}let obj = new MyClass()obj.foo()//下一个例子,只可读,不可写function readonly1(target,name,descriptor){descriptor.writable = false; // 只可读,不可写return descriptor}class Person{constructor(){this.first = 'a'this.last = 'B'}@readonly1name(){return `${this.first} ${this.last}`}}let p = new Person()console.log(p.name()) //可读//这样会报错,因为上面定义了writable = false不可改// p.name = function(){// console.log('可不可以修改name方法呢')// }// 下一个例子,日志function log(target,name,descriptor){let oldValue = descriptor.value; //这个value就是Math传过来的add方法//value是add方法,重新定义descriptor.value = function(){// ${name}是add的方法名字,arguments是add的参数console.log(`calling ${name} width`,arguments)return oldValue.apply(this,arguments)}return descriptor}class Math{//log是装饰器,先打印日志,在执行add方法//这个是装饰方法@logadd(a,b){return a + b}}let math = new Math()const result = math.add(2,5)console.log(result)//装饰器可以装饰方法和类
- Core-decorators库已经封装了es7装饰器的语法使用(相当于一个库), 可直接安装使用,像代码提到的readonly装饰器,直接引入使用即可
- 安装:npm install core-decorators --save-dev
//使用 core-decorators 库//像上面提到的readonly我们自己实现的,现在直接使用库中的import { readonly } from 'core-decorators'class Per{@readonlyname(){return '使用core-decorators'}}let per = new Per()console.log(per.name())import { deprecate } from 'core-decorators'class Per1{@deprecate('即将废弃',{url:'zhengzemin.cn'})name(){return 'zheng'}}let per1 = new Per1()per1.name()
10. 代理模式(使用者无权访问目标对象;中间加代理,通过代理做授权和控制)
- UML图
- 代理ProxyImg和ReadImg都有display方法
代码演示:
class ReadImg{constructor(fileName){this.fileName = fileName;this.loadFromDisk(); //初始化即从硬盘加载,模拟}display(){console.log('display...' + this.fileName)}loadFromDisk(){console.log('loading....' + this.fileName)}}//代理中,要有何ReadImg一样的方法display()class ProxyImg{constructor(fileName){this.readlImg = new ReadImg(fileName)}display(){this.readlImg.display(); //这样就可以直接访问ReadImg的方法}}//肯定不能直接new ReadImg对象,new代理对象let proxyImg = new ProxyImg('1.png')proxyImg.display()// 不管是用代理还是直接访问,地址都是一样,也就是说有共同的方法,这里就是display()
- 场景1:访问GitHub/内网,通过代理才可以访问 特点:不管是用代理还是直接访问,地址都是一样,也就是说有共同的方法
- 场景2:jq的proxy
- 代码演示:
<div id="div1"><a href="#">1</a><a href="#">2</a><a href="#">3</a><a href="#">4</a></div><div id="div2">159</div><script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.js"></script><script>// 使用者无权访问目标对象;中间加代理,通过代理做授权和控制// 通过div来拿到下面的a标签var div = document.getElementById('div1')div.addEventListener('click',function(e){var target = e.target;if(target.nodeName == 'A'){alert(target.innerHTML)}})// 这种肯定可以,因为this指向的是div2// $('#div2').click(function(){// $(this).css('background','red')// })//这样就无效,因为this指向的是Windows// $('#div2').click(function(){// setTimeout(function(){// $(this).css('background','red')// },100)// })//一般就是解决// $('#div2').click(function(){// var _this = this;// setTimeout(function(){// $(_this).css('background','red')// },100)// })//还有另外种解决$.proxy,遵循代理模式// $.proxy:该方法通常用于向上下文指向不同对象的元素添加事件。$('#div2').click(function(){setTimeout($.proxy(function(){$(this).css('background','red')},this),100)})</script>
- 场景3:Es6的proxy(proxy在目标对象的外层搭建了一层拦截,外界对目标对象的某些操作,必须通过这层拦截)
- 代码演示:找明星,就得先找明星经纪人(es6的proxy)
// 找明星,就得先找明星经纪人(es6的proxy)//明星let star = {name : '薛之谦',age:33,phone:'star:1591515551541'}// proxy在目标对象的外层搭建了一层拦截,外界对目标对象的某些操作,必须通过这层拦截//经纪人let agent = new Proxy(star,{get:function(target,key){//这个phone的名字必须和明星的一样,不管用不用代理,key值都是一样,像上面那个display一样//名字属性方法一定要一样if(key === 'phone'){// 返回经纪人自己的电话,明星电话获取不到return "agent:1556854544"}if(key === 'pirce'){//明星不报价,经纪人报价return 120000}return target[key] //拿到明星的信息},set:function(target,key,val){ //set多了设置的valif(key === 'customPrice'){if(val < 100000){// 最低10wthrow new Error('价格太低')}else{target[key] = valreturn true}}}})console.log(agent.name) //明星的console.log(agent.age)//明星的console.log(agent.phone) //经纪人,明星不可能给你电话console.log(agent.pirce) //经纪人给的agent.customPrice = 150000console.log(agent.customPrice) //大于15w是可以的agent.customPrice = 9000console.log(agent.customPrice) //报错,因为上面需要大于10w
- 顺带一提:(代理模式、适配器模式、装饰器模式看似都是需要一个中间件,但完全不同)
- 1. 代理模式VS适配器模式
- 适配器模式:提供一个不同的接口
- 代理模式:提供一个一样的接口
- 2. 代理模式VS装饰器模式
- 装饰器模式:扩展模式,原有的功能不变且可直接使用
- 代理模式:显示原有功能,但是经过限制或者腌割之后的
11.外观模式
- UML图:子系统都跟高层facade挂钩,先跟facade对接,在跟其他对接
代码演示:
function bindEven(elem,type,selector,fn){ if(fn == null){ fn = selector; selector = null }} //调用 bindEven(elem, 'click', '#div',fn) bindEven(elem, 'click', fn)
- 前端这种是外观模式,如果没用,就需要写了个方法,一个接受三个参数,一个接受四个; 传参不一定要传方法规定的参数,三个,四个都可以 通过高层接口bindEven来接受,实现外观模式
- 缺点:不符合接口单一原则,不可滥用
12.观察者模式(发布&订阅;一对多n) 这个模式很重要、常用
- UML图
代码演示:
// 主题,保存状态,状态变化之后触发所有观察者对象class Subject{constructor(){this.state = 0;this.observers = [] //所有观察者}getState(){return this.state;}setState(state){ //可以修改this.state = state;this.notifyAllObservers() //更新观察者}notifyAllObservers(){ // //更新观察者this.observers.forEach(observers =>{observers.update()})}attach(observer){ //添加观察者this.observers.push(observer)}}class Observers{constructor(name,subject){this.name = name;this.subject = subject;this.subject.attach(this) //添加观察者进主题}update(){console.log(`${this.name} update , ${this.subject.getState()}` )}}let s = new Subject()let o1 = new Observers('o1',s)let o2 = new Observers('o2',s)let o3 = new Observers('o3',s)s.setState(1)s.setState(2) //每次setState,都会触发所有观察者// 一对多,可以一对一,一对多
场景1:订报纸
场景2:网页事件绑定
代码演示:
先订阅着,被点击之后就执行打印,发布,click就是发布
<button id = "btn1">btn</button> <script> $('#btn1').click(function(){console.log(1)}) $('#btn1').click(function(){ console.log(2)}) </script>
- 场景3:promise
- 最后的then不会马上触发,等到成功再时再触发,使用观察者模式,先订阅着,等时期到了就触发在触发函数上
var src = "xxxx.png" var result = loadImg(src) result.then(img =>{ console.log('width', img.width) return img}).then(img => { console.log('height', img.height)})
- 场景4:jq的callback
<script>//自定义事件,自定义回调var callbacks = $.Callbacks()//add就是回调函数,像promise的then那样callbacks.add(function(info){console.log('fn1',info)})callbacks.add(function(info){console.log('fn2',info)})callbacks.fire('gogo') //对fire修改,触发add函数// fn1 gogo; fn2 gogocallbacks.fire("23:14情人节")// fn1 23:14情人节// fn2 23:14情人节</script>
- 场景5:node自定义事件(和jq的callback类似)
const EventEmitter = require('events').EventEmitterconst emitter1 = new EventEmitter()//监听 some 事件emitter1.on('some',info =>{console.log('fn1',info)})emitter1.on('some',info =>{console.log('fn2',info)})//触发 some 事件emitter1.emit('some','heheh')// fn1 heheh// fn2 heheh
- 任何class类都可以继承他使用它
//全部class都可以继承EventEmitterclass Dog extends EventEmitter{constructor(name){super()this.name = name;}}let simon = new Dog('simon')//本来没有on自定义事件,继承才有的simon.on('bark',function(){console.log(this.name,'barked')})simon.on('bark',function(){console.log(this.name,'barked1')})simon.emit('bark')
- 现在我们来看看node自己底层用到EventEmitter
- 文件的输出:node自定义事件为了获取字符
//stream 用到自定义事件//也就是说createReadStream底层自己使用了EventEmitter继承了const fs = require('fs')const readStream = fs.createReadStream('./data.txt')//就是文件太大,一次性读出来会很久,我们拿到一点,我们就读出来// 像水流一样,流一点就拿一点出来let length = 0;readStream.on('data',function(chunk){let len = chunk.toString().length //每次流出来多少字符console.log('len',len)length += len})readStream.on('end',function(){console.log('length',length)})
- readline拿到文件有几行
const fs = require('fs')const readline = require('readline')let rl = readline.createInterface({input:fs.createReadStream('data.txt')})let lineNum = 0rl.on('line',function(line){// console.log(line) //就是每行的文件内容lineNum++})rl.on('close',function(){console.log('文件有'+lineNum+'行')})
- 场景6:http请求
- 场景7:其他场景
场景8:Vue组件生命周期触发,vue的watch
先把firstName放在那里,看看等下会不会set改变,会就发布执行观察者
13. 迭代器模式(顺序访问一个集合(一般是数组,对象不是有序);使用者无需知道集合的内部结构(封装))
- UML图
代码演示:
class Iterator{constructor(container){this.list = container.list;this.index = 0;}next(){if(this.hasNex()){ //如果有下一个就循环返回listreturn this.list[this.index++]}return null; //如果没用下一项就返回null}//是否有下一项hasNex(){if(this.index == this.list.length){return false;}return true;}}class Container{constructor(list){this.list = list;}//生成迭代器getIterator(){return new Iterator(this);}}let arr = [1,2,3,4]let constainer = new Container(arr)let iterator = constainer.getIterator()while(iterator.hasNex()){console.log(iterator.next())}//现在不仅仅是可以传数组,因为我们进行封装// 兼容所有的有序集合
- 场景1:jQuery each
- 如何写出一个方法循环以下三种对象呢?
<div id="div1"><a href="#">1</a><a href="#">2</a><a href="#">3</a><a href="#">4</a></div><div id="div2">159</div><script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.js"></script><script>var arr = [1,2,3]var nodeList = document.getElementsByTagName('a')var $a = $('a')//现在进行循环他们// arr.forEach(function(data,key){// console.log(data)// })// // nodeList不是数组,不能foreach// for(var i = 0;i < nodeList.length;i++){// console.log(nodeList[i])// }// $a.each(function(data,key){// console.log(data,key)// })//现在使用迭代器,将他们用一种方法,可以遍历这这个function each(data){//重要将data转为jq对象,就可以使用each循环var $data = $(data) //生成迭代器$data.each(function(key,data){console.log(key,data)})}each(arr)each(nodeList)each($a)// 顺序遍历有序集合// 使用者不必知道集合的内部结构// 像我们这个不需要知道是不是数组还是什么,丢什么给我,都可以遍历</script>
- 场景2:Es6 iterator迭代器
- ES6 Iterator为何存在?
- es6语法中,有序集合的数据类型已经有很多
- Array、Map、Set String、TypedArray、arguments、NodeList
- 需要有一个统一的遍历接口来遍历所有数据类型(注意,object不是有序集合,可以用map代替)
- 以上数据类型,都有[Symbol.iterator]属性
- 属性值是函数,执行函数返回一个迭代器
- 这个迭代器就有next方法可顺序迭代子元素
- 可运行Array.prototype[Symbol.iterator]来测试
- 代码演示:
// 现在讲解es6的Iterator迭代器function each1(data){//生成迭代器let iterator = data[Symbol.iterator]()//done:true 就是没数据了,最后一项了let item = {done:false} //先手动添加个done,来作为while的循环条件while(!item.done){item = iterator.next()if(!item.done){//这里为false就是,还有值,就可以打印出console.log(item.value)}}}let aaarr = [2,3,4]let m = new Map()m.set('a',100)m.set('b',200)each1(aaarr)each1(m)
- 抛出问题,难道每个es6开发者都要自己封装这个使用吗?
- 肯定不会。
- 作者将封装在for of里面
这段代码是上面each那个Symbol.iterator的简写
//Symbol.iterator,并不是人人都知道//也不是每个人都需要封装一个each方法
//因此有了fro..of语法
function each1(data){
//要想使用for of,首先data打印出来,必须有Symbol.iterator方法// 带有变流器特征对象,data[Symbol.iterator]有值for(let item of data){console.log(item)}}each1(aaarr)each1(m)
- For of是遍历迭代器,for in是遍历对象
- Iterator也被Generator使用,Generator也拥有Iterator的特性
14.状态模式(一个对象有状态变化;每次状态变化都会触发一个逻辑;不能总是用if.else来控制)
- UML图
代码演示:交通灯变化
// 状态(红灯、绿灯)class State{constructor(color){this.color = color;}handle(context){console.log(`${this.color}`)context.setState(this)}}//主体class Context{constructor(){this.state = null;}//状态获取getState(){return this.state}setState(state){this.state = state;}}//把主体和状态分离开// 主体可以get set状态,状态变化的逻辑让状态去写//测试let context = new Context()let green = new State('green')let red = new State('red')let yellow = new State('yellow')//绿灯亮了green.handle(context)console.log(context.getState()) //打印状态//红灯亮了red.handle(context)console.log(context.getState()) //打印状态
- 场景2:有限状态机
- 有限个状态、以及这些状态之间的变化
- 如:交通信号灯
- 使用开源lib:javaScript-state-machine
- github.com/jakesgordon/javaScript-state-machine
- 安装javascript-state-machine库:npm i javascript-state-machine --save-dev
- 例子:根据javascript-state-machine库,看例子看特性写法
代码演示:
//有限状态机import StateMachine from 'javascript-state-machine'import $ from 'jquery'//像取消关注’取消点赞这些都可以用这个//初始化状态机模型let fsm = new StateMachine({init:'收藏',transitions:[ //不要写欠个s{name:'doStore', //name的方法和methods的onDoStore名字一样,on加name,不过是驼峰命名from:'收藏',to:'取消收藏'},{name:'deleteStore',from:'取消收藏',to:'收藏'}],methods:{//监听执行收藏onDoStore:function (){ //方法名字和transition的name一样,on加name,不过是驼峰命名alert('收藏成功'); //可以post请求,数据库更新updateText()},onDeleteStore:function (){alert('取消收藏成功'); //可以post请求,数据库更新updateText()}}})let btn = $('#btn1')btn.click(function(){if(fsm.is('收藏')){fsm.doStore()}else{fsm.deleteStore()}})//更新按钮文案function updateText(){btn.text(fsm.state)}//初始化文案updateText()
- 场景3:promise就是个有限状态机,有pending、fullfilled、rejected..三个状态
// promise 有限状态机//状态机模型let fsmPromise = new StateMachine({init:'pending', //初始化状态transitions:[{name: 'resolve', //name是事件名称from: 'pending',to: 'fullfilled'},{name: 'reject',from: 'pending',to: 'rejected'}],methods:{onResolve: function(state,data){// 参数:state - 当前状态机实例:data - fsmPromise.resole(xxx)执行时传递过来的参数,data就是xxx//通过data获取调用fn即可data.successList.forEach(fn => fn());},//失败函数onReject: function (state,data){// 参数:state - 当前状态机实例:data - fsmPromise.reject(xxx)执行时传递过来的参数data.faillList.forEach(fn => fn());}}})// promise是个类,因为是new出来的,接受一个函数,这个函数有两个参数class myPromise{constructor(fn){this.successList = [];this.faillList = [];//这两个是fn的参数fn(() =>{//resolve函数fsmPromise.resolve(this)},() =>{fsmPromise.reject(this)})}//then的两个函数不会马上触发,所以先将他们存起来then(successFn,failFn){this.successList.push(successFn)this.faillList.push(failFn);}}//测试function loadImg(src){//这里传一个函数,需要马上执行,所以这个函数写在constructor上const promise = new myPromise(function(resolve,reject){let img = document.createElement('img')img.onload = function(){resolve(img)}img.onerror = function(){reject()}img.src = src;})return promise}let src = "https://dss0.baidu.com/73x1bjeh1BF3odCf/it/u=4003888963,1806138384&fm=85&s=9102FE5E6413E3CE9E3E1911030010DE"let result = loadImg(src) //返回一个promise对象//then的函数在resolve下才执行,第二个是在reject下执行的result.then(function(){console.log('ok1')},function(){console.log('fail1')})result.then(function(){console.log('ok2')},function(){console.log('fail2')})
以下模式不常用
先不画上UML图了
15.原型模式(clone自己,生成一个新对象(new开销比较大))
- 例子:Object.create
- 对比JS中的原型prototype
- prototype可以理解为es6 class的一种底层原理
- 而class是实现面向对象的基础,并不是服务于某个模式
- 其实object.create底层也是prototype,但是class不是为了某种模式创建的,是面向对象,而object.create就是为了原型模式设计的。两者基础不一样的
- 为什么说原型模式是不常用的?因为一般都是可以new出来的,而原型模式要clone出来
- 代码演示:
// 一个原型 对象const prototype = {getName:function(){return this.first},say:function(){console.log('hellow')}}//基于原型创建x,一个全新的对象let x = Object.create(prototype)x.first = 'A'console.log(x.getName()) //因为他是相当于本prototype复制过来,所以可以打印firstx.say()//在复制一个let y = Object.create(prototype)y.first = 'B'console.log(y.getName())y.say()
16. 桥接模式(用于把抽象化与实现化解耦;使得二者可以独立变化)
场景:画图
- 应该把画图分成先画形状,在画颜色,而不是把下面四个图都一起画出来,如果以后多了10个颜色的,就很难维护
所以应该把画图分成先画形状,在画颜色,最后才会形成有颜色的图形
代码演示:
class Color{constructor(name){this.name = name;}}class Shape{constructor(name,color){this.name = name;this.color = color;}draw(){console.log(`${this.color.name} ${this.name}`)}}//测试let red = new Color('red');let yellow = new Color('yello');let circle = new Shape('circle',red);circle.draw()let triangle = new Shape('triangle',yellow)triangle.draw()
17.组合模式(生成树形结构,表示“整体-部分”关系;让整体和部分都具有一致的操作方式)
- 场景1:树形结构让整体和部分都具有一致方式
- 场景2:DOM的vnode
- 场景3:整体和单个节点的操作是一致的;整体和单个节点的数据结构也保持一致
18.亨元模式(共享内存(主要考虑内存,而非效率);相同的数据,共享使用)
- 设计原则验证:将相同的部分抽象出来(符合开放封闭原则 ) 改一个地方,其他地方也会生效
19.策略模式(不同策略分开处理;避免出现大量if...else或者switch...case)
- 代码演示:
class User{constructor(type){this.type = type;}buy(){//频繁用到if,可以考虑将这三种分开写个类,这样可扩展性,什么用户什么类if(this.type === "ordinary"){console.log('普通用户购买')}else if(this.type ==="member"){console.log('会员用户购买')}else if(this.type ==="vip"){console.log('vip用户购买')}}}let u1 = new User('ordinary');u1.buy()let u2 = new User('member');u2.buy()let u3 = new User('vip');u3.buy()console.log('--------------什么用户什么类')// 策略模式(不同策略分开处理;避免出现大量if...else或者switch...case)//上面频繁用到if,可以考虑将这三种分开写个类,这样可扩展性,什么用户什么类class Ordinary{buy(){console.log('普通用户购买')}}class Member{buy(){console.log('会员用户购买')}}class Vip{buy(){console.log('VIP用户购买')}}//使用什么就用什么的用户let user1 = new Ordinary()user1.buy()let user2 = new Member()user2.buy()let user3 = new Vip()user3.buy()
20.模板方法模式(公用的东西合起来调用)
- 场景1:如果要执行handle1 2 3都用的话,就直接写在handle方法中,一起调用这是三个方法
class Action{handle(){handle1()handle2()handle3()}handle1(){console.log(1)}handle2(){console.log(2)}handle3(){console.log(3)}}
21.职责链模式(一步操作可能分位多个职责角色来完成;把这些角色都分开,然后用一个链串起来,将发起者和各个处理者进行隔离)
- 代码演示:
//请假要组长,然后经理,总监class Action{constructor(name){this.name = name;this.nextAction = null;}setNextAction(action){this.nextAction = action;}handle(){console.log(`${this.name}审批`)if(this.nextAction!=null){this.nextAction.handle()}}}//测试let a1 = new Action('组长')let a2 = new Action('经理')let a3 = new Action('总监')a1.setNextAction(a2)a2.setNextAction(a3)a1.handle()// a1里面要a2,a2有a3// 通过nextAction将这些串起来// handle这样子写,是为了实现a1 a2 a3这样链操作// 一步操作可能分位多个职责角色来完成;把这些角色都分开,// 然后用一个链串起来,// 将发起者和各个处理者进行隔离
- 场景:Jquery的链式操作;poomise.then的链式操作
22.命令模式(执行命令时,发布者和执行者分开;中间加入命令对象,作为中转站) 发送者直接去调用接受者有点麻烦,所以就通过命令对象
- 代码演示:
// 接受者class Receiver{exec(){console.log('执行')}}//命令者class Command{constructor(receiver){this.receiver = receiver}cmd(){console.log('执行命令');this.receiver.exec()}}//触发者class Invoker{constructor(command){this.command = command;}invoke(){console.log('开始')this.command.cmd()}}// 触发者 - > 命令者 -> 接受者//士兵let soldier = new Receiver()let command = new Command(soldier)let invoker = new Invoker(command)invoker.invoke()// 开始// 执行命令// 执行
- 场景:js中的应用
- 1. 网页富文本编辑器操作,浏览器封装了一个命令对象
- 2. document.exeCommand('bold') 加粗
- 3. document.exeCommand('undo') 撤销
- 分析:现在这种相当于我就是将军,调用exeCommand, 然后他自己去执行命令,骑士,然后就去找士兵,这样子一系列操作,浏览器底层就是士兵他们干的,撤销什么的
- 富文本就是命令模式,点一下加粗就加粗
23.备忘录模式(随时记录一个对象的状态变化;随时可以恢复之前的某个状态(如撤销功能,编辑器保存和撤回))
- 代码演示:
//备忘录类class Menento {constructor(content){this.content = content}getContent(){return this.content;}}//备忘录列表class CareTaker{constructor(){this.list = []}add(menento){ //将编辑器需要备忘的存进来this.list.push(menento);}get(index){return this.list[index];}}//编辑器class Editor{constructor(){this.content = null;}//设置内容setContent(content){this.content = content;}getContent(){return this.content;}//保存saveContentToMenento(){return new Menento(this.content);}//恢复getContentFromMemento(menento){this.content = menento.getContent()}}let editor = new Editor()let careTaker = new CareTaker();editor.setContent("111")editor.setContent("222")careTaker.add(editor.saveContentToMenento()) //将当前内容备份editor.setContent('333')careTaker.add(editor.saveContentToMenento())editor.setContent('444')console.log(editor.getContent())editor.getContentFromMemento(careTaker.get(1))console.log(editor.getContent())editor.getContentFromMemento(careTaker.get(0))console.log(editor.getContent())
24. 中介者模式
- 场景:买房子和租房找中介一样
- 代码演示:a要去修改b,得通过中介者去修改,b也是一样
class A{constructor(){this.num = 0;}setNumber(num,m){this.num = num;if(m){m.setB()}}}class B{constructor(){this.num = 0;}setNumber(num,m){this.num = num;if(m){m.setA()}}}//中介者class Mediator{constructor(a,b){this.a = a;this.b = b;}setA(){let num = this.b.numthis.a.setNumber(num+100)}setB(){let num = this.a.numthis.b.setNumber(num+100*2)}}let a = new A()let b = new B()let m = new Mediator(a,b)a.setNumber(100,m)console.log(a.num,b.num) // 100 300b.setNumber(100,m)console.log(a.num,b.num) // 200 100
不常用的两种
25. 访问者模式(将数据操作和数据结构进行分离)
26. 解释器模式(描述语言语法如何定义,如何解决和编译)
- 场景:es6解析成es5 使用babel
27.面试:
- 工厂和单例和观察者是最重要的(能说出五种模式就很了不起了)
- 日常使用 重点讲解的设计模式,要强制自己模仿、掌握、刻意练习 不要用而用,设计就是为了简单而设计