前端JavaScript设计模式

312 阅读20分钟

前言

  • 面试的时候面试官问你了解设计模式多少?
  • 而我却含含糊糊、吞吞吐吐说了些面试官并不想听到的答案
  • 所以为了不在面试的时候尴尬,小编带领大家了解一下JS设计模式,让大家在面试的时候胸有成竹!
  • 建议大家先收藏,有空的时候look一look,字有点长,笔芯~


27个方面

1. 搭建开发环境

  1. 因为这里想在编辑器上更好的演示设计模式,所以这里简单的搭下环境(webpack、babel(为了让es6转为es5))。
  2. 初始化

     npm init
  3. webpack安装:

    npm i webpack webpack-cli --save-dev
  4. webpack-dev-server和html-webpack-plugin安装:
  5. npm install webpack-dev-server html-webpack-plugin --save-dev
    (webpack不会实时更新,而webpack-dev-server会实时更新; html-webpack-plugin网页模板的插件)
  6. babel(es6 -> es5):

    npm install babel-core babel-loader babel-polyfill babel-preset-es2015 babel-preset-latest --save-dev
  7. 此时可以npm run dev,但可能npm会提示需要再安装babel-loader,如果遇到就:

    npm install babel-loader@7
  • webpack.dev.config配置
  • 添加之后,开源npm run dev调试
  • es6转为es5

2. 了解面向对象

  1. 对象 == 类(模板),类中有属性和方法
  2. 如何生成对象? 通过new对象(实例)
  3. 面向对象三要素:封装继承多态
    封装:三要素public、private、protected(对子类开放),目前es6不支持,一般认为_开头的属性是private
    多态:父类的方法不实现,让子类按自己的需求自己去实现


  4. 为何使用面向对象?
    1. 程序执行:顺序、判断、循环 --- 结构化
    2. 面向对象  --- 数据结构化
    3. 对于计算机,结构化的才是最简单的
    4. 变成应该 简单 & 抽象

3.设计原则

  1. 使用设计模式,要画UML图,这里介绍两种画图工具:

    需要下载的软件:ms office visio
    在线制作:www.processon.com/diagrams
    画图规范可以按照这个模板写类Class


  2. 先画图,再写代码

  3. 说到设计原则,这里必须提下《unix/linux设计哲学》这本书,告诉我们什么是设计
    1. 舍弃高效性而取可移植性:写好一个东西可以复制去别的地方用,比,一个东西效能速度好,但是移植性差,因为老是要改,所以移植性比较重要
    2. 采用纯文本存数据
    3. 允许用户去定制环境(环境不能写死,像那些配置背景什么的)
    4. 沉默是金:返回是数字,如果返回个“这不是数字”字符串就不行,因为人家就是要返回number,是字符串就不要返回,返回个-1也行
    5. 需求90%的解决方案:花20%的成本解决80%的问题,如果就要解决剩下的20%的问题,就要花很大力气去解决,没必要,能满足大部分人的需求就好,没必要做到完美 

  4. 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.place
place.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 = index
this.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 instance1
return 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'
}

@readonly1
name(){
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方法
//这个是装饰方法
@log
add(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{
@readonly
name(){
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多了设置的val
if(key === 'customPrice'){
if(val < 100000){
// 最低10w
throw new Error('价格太低')
}
else{
target[key] = val
return true
}
}
}
})


console.log(agent.name) //明星的
console.log(agent.age)//明星的
console.log(agent.phone) //经纪人,明星不可能给你电话
console.log(agent.pirce) //经纪人给的

agent.customPrice = 150000
console.log(agent.customPrice) //大于15w是可以的

agent.customPrice = 9000
console.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 gogo
callbacks.fire("23:14情人节")
// fn1 23:14情人节
// fn2 23:14情人节
</script>
  • 场景5:node自定义事件(和jq的callback类似)
const EventEmitter = require('events').EventEmitter

const 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都可以继承EventEmitter
class 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 = 0
rl.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()){ //如果有下一个就循环返回list
return 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复制过来,所以可以打印first
x.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.num
this.a.setNumber(num+100)
}
setB(){
let num = this.a.num
this.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 300
b.setNumber(100,m)
console.log(a.num,b.num) // 200 100


不常用的两种 

25. 访问者模式(将数据操作和数据结构进行分离)

26. 解释器模式(描述语言语法如何定义,如何解决和编译)

  •  场景:es6解析成es5 使用babel 


27.面试:

  •  工厂和单例和观察者是最重要的(能说出五种模式就很了不起了)
  •  日常使用 重点讲解的设计模式,要强制自己模仿、掌握、刻意练习 不要用而用,设计就是为了简单而设计