UML类图到深入代码演示,攻陷前端常见设计模式

2,779 阅读10分钟

前言

设计模式无论对于前端后端,都是必须理解的一种编码理念,作为前端开发人,其实在日常开发中不知不觉已经采用了设计模式的理念,理解好设计模式,运用设计模式,是一个开发人员质的提升

本文从概念到生活中场景以及编码场景再到示例,解析前端常用9种设计模式,第一步总览设计模式的前传,也就是面向对象概念以及设计原则,然后对每一个设计模式从介绍到UML类图再到场景与示例,最后对设计原则的验证,让各位对设计模式有一个更深入的了解,能够在日常开发中运用

GITHUB: github.com/Jason9708/j…

文章以代码为主,所以阅读本文需要有一定的JavaScript基础(ES6)

面向对象

三要素

  • 继承:子类继承父类
    • 父类是公共的,不仅仅只服务于一个子类
    • 继承可以将公共方法抽离出来,提高复用性,减少冗余
  • 封装:数据的权限和保密(Javascript,甚至Es6本身都没有封装这个概念,但Typescript可实现封装)
    • 关键字(定义属性/方法)
      • public 完全开放
      • protected 对子类开放
      • private 对自己开放
    • 特点
      • 减少耦合,不该暴露的不暴露
      • 利于数据,接口的权限管理
      • Es不支持,我们可以去约定某种形式( _ 开头)去告诉合作者这是private类型
  • 多态:同一接口不同实现
    • Js对于多态的应用极少,因为需要结合Java等语言的接口,重写,重载等功能
    • 未来Ts的大范围普及,多态的功能也会更加显著
    • 特点
      • 保持子类的开放性和灵活性
      • 面向接口编程(Js使用较少)

Js应用举例

JQuery就是一个class,而${'p'}就是JQuery的一个实例

class JQuery {
    constructor(selector) {
            let slice = Array.prototype.slice
            let dom = slice.call(document.querySelectorAll(selector)) // 获取每一个dom元素
            let len = dom ? dom.length : 0
            for (let i = 0; i < len; i++) {
                this[i] = dom[i]
            }
            this.length = len
            this.selector = selector || ''
        }
        // JQuery自身API
    append(node) {
        // TODO
    }
    addClass(name) {
        // TODO
    }
    html(data) {
            // TODO
        }
        // ...
}

window.$ = function(selector) {
    return new JQuery
}
使用
var $p = $('p')
$p.append(xxx)

为何要使用面向对象

  • 面向对象是为了解决系统的可维护性,可扩展性,可重用性
  • 面向对象实现了一种数据的结构化,是为了表述、模拟世间万事
  • 面向对象的意义是将平整的数据进行结构化。对于计算机而已,结构化才是最简单的、
  • 编程应该做到 简单 & 抽象
好处:
当需求单一,或者简单时我们一步一步操作没有问题,并且效率也挺高。可随着需求的更改,功能的增多,发现于鏊面对每一个步骤很麻烦了,这时就开始思索能不能把步骤和功能进行封装。封装时根据不同的功能,进行不同的封装,功能类似的封装在一起。这样结构就清晰了很多。用的时候找到对应的类就可以了。

设计原则

何为设计?

  • 按照哪一种思路或者标准来实现功能
  • 功能相同,可以有不同的设计方案来实现
  • 随着需求的增加,设计的作用才能体现出来

五大设计原则 (SOLID)

❗ 五大设计原则可以说是设计模式的基础,学好设计模式应该先理解五大设计原则

S - 单一职责原则
        - 一个程序只做好一件事
        - 如果功能过于复杂就拆分开,每个部分保持独立
O - 开放封闭原则
        - 对扩展开放,对修改封闭(重点)
        - 增加需求时,尽量做到扩展新代码,而非修改已有代码
L - 李氏置换原则
        - 子类能覆盖父类
        - 父类能出现的地方子类就能出现
        - 由于Js是弱类型,以及继承使用较少,所以在Js中应用较少
I - 接口独立原则
        - 保持接口的单一独立,功能统一
        - Js中没有接口(TypeScript除外),使用较少
        - 类似于单一职责原则,但更关注于接口
D - 依赖导致原则
        - 面向接口编程,依赖于抽象而不依赖于具体
        - 使用方只关注接口而不关注具体类的实现
        - Js中使用较少(没有接口 & 弱类型)

JavaScript中:S O体现较多 L I D体现较少

用 Promise 来说明 S 与 O

// 加载图片
function loadImg(src) {
    var promise = new Promise((resolve,reject) => {
        var img = document.createElement('img')
        img.onload = () => {
            resolve(img)
        }
        img.onerror = () => {
            reject('图片加载失败')
        }
        img.src = src
    })
    return promise
}
var src = '...'
var result = loadImg(src)
result.then(img => {
    console.log('width:',img.width)
    return img
}).then(img => {
    console.log('height:',img.height)
}).catch( err => {
    // 捕获异常
    console.log(err)
})

观察上面这个例子,我们可以发现

单一职责原则体现在我们每一个then只做一件事,如果上面还想再做第三件事,我们就再加一个then,而不是在第一个或第二个then中多做一件事

开放封闭原则体现在如果我们有新的需求进来,我们不需要去修改第一第二个then,而是直接添加一个then去实现

总结:
单一职责原则:每一个 then 中的逻辑只做好一件事
开放封闭原则:如果新增需求,扩展 then

23种设计模式简介

  • 创建型
    • 工厂模式(工厂方法模式,抽象工厂模式,建造者模式)
    • 单例模式
    • 原型模式
  • 结构型
    • 适配器模式
    • 装饰器模式
    • 代理模式
    • 外观模式
    • 桥接模式
    • 组合模式
    • 享元模式
  • 行为型
    • 策略模式
    • 模板方法模式
    • 观察者模式 (Js应用较多)
    • 迭代器模式 (Js应用较多)
    • 职责链模式
    • 命令模式
    • 备忘录模式
    • 状态模式 (Js应用较多)
    • 访问者模式
    • 中介者模式
    • 解释器模式

工厂模式

介绍

  • new操作单独封装
  • 遇到new的时候就可以考虑是否使用工厂模式

示例

  • 去购买汉堡的时候,直接点餐,取餐,不需要自己亲手做
  • 商店封装做汉堡的工作,做好直接给买者

UML类图

代码演示

class Product {
    constructor(name) {
        this.name = name
    }
    init() {
        console.log('init')
    }
    fun1() {
        console.log('fun1')
    }
    fun2() {
        console.log('fun2')
    }
}

class Creator {
    create(name) {
        return new Product(name)
    }
}

// Test
let creator = new Creator()
let product = creator.create('product')
product.init()
product.fun1()
product.fun2()

场景

  • Jquery $('div')就是一个工厂
// Product
class JQuery {
    constructor(selector) {
            let slice = Array.prototype.slice
            let dom = slice.call(document.querySelectorAll(selector)) // 获取每一个dom元素
            let len = dom ? dom.length : 0
            for (let i = 0; i < len; i++) {
                this[i] = dom[i]
            }
            this.length = len
            this.selector = selector || ''
        }
        // JQuery自身API
    append(node) {
        // TODO
    }
    addClass(name) {
        // TODO
    }
    html(data) {
        // TODO
    }
    // ...
}
// Creator
window.$ = function(selector) {
    return new JQuery
}
  • React.createElement
// Product
class Vnode(tag,attrs,children) {
    // ...
}
// Creator
React.createElement = function(tag, attrs, children) {
    return new Vnode(tag, attrs, children)
}
  • vue 异步组件
Vue异步组件有这样的用法

Vue.component('Component',function(resolve,reject){
    setTimeout(() => {
        resolve({
            template:'<div> I am Component </div>'
        })
    },1000)
})

设计原则验证

工厂模式中构造函数与创建者分离,通过工厂方法将构造函数与用户隔离开,让用户不能去修改构造函数,符合开放封闭原则


单例模式

介绍

  • 系统中被唯一使用
  • 一个类只能初始化一个实例

示例

  • 登陆框(再复杂,也只能有一个)
  • 购物车(一个商城app,也是只能有一个购物车)

场景

  • JQuery中只有一个 $
// JQuery 只有一个 $
if(window.jQuery != null){
    return window.jQuery
}else{
    // 初始化...
}


这里的代码实现与我们的单例模式不一样,但思想是一样的(有则返回,无则创建)
  • 模拟登录框(登陆框只有一个)
// 使用单例思想
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 = (() => {
    let instance
    return function() {
        if(!instance) {
            instance = new LoginForm()
        }
        return instance
    }
})()
  • 其他
    • 购物车(和登录框类似)
    • vuexredux中的store

设计原则验证

只实例化唯一的对象,符合单一职责原则

没法具体体现开放封闭原则,但是绝不违背开放封闭原则


适配器模式

介绍

  • 旧接口格式与现使用者不兼容
  • 中间加一个适配来转换接口

通俗来像就有点像我们的数据线转换器

UML类图

代码演示

class Adaptee {
    specificRequest() {
        return '标准插头'
    }
}

class Target {
    constructor() {
        this.adaptee = new Adaptee()
    }
    request() {
        let info = this.adaptee.specificRequest
        return `${info} - 转换 → 大号插头`
    }
}

场景

  • 封装旧接口
// 自己封装的ajax,使用方法如下
ajax({
    url:'/getData',
    type:'Post',
    dataType:'json',
    data: {
        id:'123'
    }
}).done(() => {

})

// 但因为历史原因,代码都是
$.ajax({
    //...
})

// 即做了一层适配器
var $ = {
    ajax: function(options){
        return ajax(options)
    }
}
  • Vue computed
// 官方computed例子
<div id='example'>
    <p>Original message: "{{message}}"</p>
    <p>Computed reversed message:"{{reversedMessage}}"
</div>


var vm = new Vue({
    el: '#example',
    data: {
        message: 'Hello'
    },
    computed: {
        // 计算属性的 getter
        reversedMessage:function() {
            // this 执行 vm实例
            return this.message.split('').reverse().join('')
        }
    }
})

从模板我们可以知道 我们需要一个普通的message,还需要一个倒序的message

因此data中的message不足以满足我们的需求,我们需要再做处理(封装旧接口思想)

这里computed就相当于一个适配器

设计原则验证

将旧接口和使用者进行分离,使用者不再操作旧接口,符合开放封闭原则


装饰器模式

介绍

  • 为对象添加新功能
  • 不改变其原有的结构和功能

与适配器不同,适配器是旧接口已经不能用了,或者说数据,数据格式不能用,所以需要重新设计一个去用,而装饰器模式是原有的功能我们还会继续用,在其基础上再添加功能

UML类图

代码演示

// 被装饰者
class Circle {
    draw() {
        console.log('画一个圆形')
    }
}
// 装饰器
class Decorator {
    constructor(circle) {
        this.circle = circle
    }
    setRedBorder(circle) {
        console.log('设置红色边框')
    }
    draw() {
        this.circle.draw()
        this.setRedBorder(circle)
    }
}

// Test
let circle = new Circle()
circle.draw()

let decorator = new Decorator()
decorator.draw()

场景

  • ES7装饰器
  • 第三方库 core-decorators

ES7 装饰器解析

  • 配置环境
npm install babel-plugin-transform-decorators-legacy --save-dev

.babelrc

{
    "presets": [
        "es2015",
        "latest"
    ],
    "plugins": [
        "transform-decorators-legacy"
    ]
}
  • 装饰类
// 例1:

// 对Demo类进行修饰
@testDec
class Demo {
    // ... 
}

function testDec(target) {
    target.isDec = true
}

console.log(Demo.isDec) // true

// 可以加参数
// 装饰器
function testDec(isDec) {
    return function(target) {
        target.isDec = isDec
    }
}
// Demo - 被装饰者
@testDec(false)
class Demo {

}
console.log(Demo.isDec) // false
// 例2: mixin示例

function mixins(...list) {
    return function(target) {
        Object.assign(target.prototype, ...list)
    }
}

const Foo = {
    foo() {
        console.log('foo')
    }
}

@mixins(Foo)
class MyClass {

}

let obj = new MyClass()
obj.foo() // 'foo'
  • 装饰方法
// 例1:

function readonly(target, name, descriptor) {
    // descriptor 属性描述对象 ( Object.defineProperty中会用到),原来的值如下
    // {
    //     value: specifiedFunction,
    //     enumerable:false,
    //     configurable: true,
    //     writable: true
    // }
    descriptor.writable = false
    return descriptor
}

class Person {
    constructor() {
        this.first = 'A'
        this.last = 'B'
    }

    // 装饰方法
    @readonly   // 将name属性的writable装饰为false
    name() {
        return `${this.first} - ${this.last}`
    }
}

var p = new Person()
console.log(p.name())   // A - B
// p.name = function() {}   // 这里会报错,是因为 name函数被装饰为只读属性
// 例2:
function log(target, name, descriptor) {
    var oldValue = descriptor.value

    descriptor.value = function() {
        console.log(`Calling ${name} with`, arguments)
        return oldValue.apply(this,arguments)
    }
    return descriptor
}

class Math {
    // 装饰方法
    @log    // oldValue → add()  经过装饰之后,会先打印日志,然后执行oldValue,也就是add()
    add(a, b) {
        return a + b
    }
}

const math = new Math()
const result = math.add(2, 4)   // 执行add时,会自动打印日志,因为add已经被log装饰器装饰过
console.log(result)

/*
    Result:
    Calling add with Arguments(2) [2, 4, callee: (...), Symbol(Symbol.iterator): ƒ]
    6
*/

core-decorators

  • 第三方开源依赖
  • 提供常用的装饰器
  • 示例
// 首先安装 npm i core-decorators --save

// 开始编码
import { readonly } from 'core-decorators'

class Person {
    @readonly
    name() {
        return 'zhang'
    }
}

let p = new Person()
console.log(p.name())
// p.name = function() {}   // 这里会报错,是因为 name函数被装饰为只读属性
// deprecate 执行前有警告提示
import { deprecate } from 'core-decorators'

class Person {
    @deprecate
    facepalm(){}

    @deprecate('We stopped facepalming')
    facepalmHard(){}
}

设计原则验证

装饰器模式将现有对象和装饰器进行分离,两者是独立存在的,装饰的时候不去修改原有的对象,符合开放封闭原则


代理模式

介绍

  • 使用者无权访问目标对象
  • 中间加代理,通过代理做授权和控制

UML类图

代码演示

class ReadImg {
    constructor(fileName) {
        this.fileName = fileName
        this.loadFromDisk()  // 初始化即从硬盘中加载
    }
    loadFromDisk() {
        console.log('loading...',this.fileName)
    }
    display() {
        console.log('display...',this.fileName)
    }
}

// 代理
class ProxyImg {
    constructor(fileName) {
        this.realImg = new ReadImg(fileName)
    }
    display() {
        this.realImg.display()
    }
}

// Test
let proxyImg = new ProxyImg('1.png')
proxyImg.display()

/*
    Result:
    loading...1.png
    display...1.png
*/

场景

  • 网页事件代理
<div id='div1'>
    <a href='#'>a1</a>
    <a href='#'>a2</a>
    <a href='#'>a3</a>
    <a href='#'>a4</a>
</div>

<script>
    var div1 = document.getElementById('div1')
    div1.addEventListener('click',function(e) {
        var target = e.target
        if(target.nodeName === 'A'){
            alert(target.innerHtml)
        }
    })
</script>
  • JQuery $.proxy
$('#div1').click(function() {
    // this 符合期望
    $(this).addClass('red')
})
$('#div1').click(function() {
    setTimeout( function() {
        // this 不符合期望
        $(this).addClass('red')
    },1000)
})

// 可以用如下方式解决
$('#div1').click(function() {
    var _this = this 
    setTimeout( function() {
        // this 符合期望
        $(_this).addClass('red')
    },1000)
})

// 也可以通过代理 $.proxy 解决
$('#div1').click(function() {
    setTimeout( $.proxy(function() {
        // this 符合期望
        $(this).addClass('red')
    },this),1000)
})
  • ES6 Proxy
// 明星 & 经纪人 案例
// 顾客要聘请明星,不能直接与明星接触,而是与明星经纪人接触

// 明星
let star = {
    name: '张三',
    age: 20,
    phone: '13000000000'
}

// 经纪人
let agent = new Proxy( star, {
    get: function(target, key){
        if(key === 'phone'){
            // 返回经纪人自己的手机号
            return '10086'
        }
        if(key === 'price'){
            // 明星自己不报价,由经纪人来报价
            return 120000
        }
        return target[key]
    },
    set: function(target, key, val){
        if(key === 'customPrice){
            if(val < 100000) {
                // 最低 10w
                throw new Error('顾客报价太低')
            }else {
                target[key] = val
                return true
            }
        }
    }
})

// Test
console.log(agent.name)  // 张三
console.log(agent.age) // 20
console.log(agent.phone) // 10086
console.log(agent.price) // 120000

agent.customPrice = 80000 // 抛错

设计原则验证

代理类和目标类分离,将目标类与使用者隔离开,符合开放封闭原则


代理模式、适配器模式、装饰器模式对比

代理模式 VS 适配器模式

  • 适配器模式:提供一个不同的接口,旧的无法使用,需要进行转换(如不同版本的插头)
  • 代理模式:提供一模一样的接口(使用者无权使用目标类,通过一个代理,来访问到目标类的效果)

代理模式 VS 装饰器模式

  • 装饰器模式:扩展功能,原有功能不变且可直接使用
  • 代理模式:显示原有功能,但是经过限制或者阉割之后的(哪些可以访问,哪些不可以访问,有一定限制)

外观模式

介绍

  • 为子系统中的一组接口提供了一个高层接口
  • 使用者使用这个高层接口

示例

生活中去医院看病,接待员去挂号,门诊,取药

UML类图

场景

function bindEvent(elem, type, selector, fn) {
    // 解决必须传4个参数的情况
    if(fn == null) {
        fn = selector
        selector = null
    }

    // ...
}

// 调用
bindEvent(elem, 'click', '#div1', fn)
bindEvent(elem, 'click', fn)

设计原则验证

  • 不符合单一职责原则和开放封闭原则,因此需要谨慎使用,不可滥用

观察者模式

介绍

  • 发布 & 订阅
  • 一对多

示例

  • 点咖啡,点好之后坐等被叫

UML类图

代码演示

// 主题,保存状态,当状态发生变化后,触发所有观察者对象
class Subject {
    constructor() {
        this.state = 0
        this.observers = []
    }
    getState() {
        return this.state
    }
    // 当state发生改变,触发所有观察者
    setState(state) {
        this.state = state
        this.notifyAllObservers()
    }
    // 触发观察者函数
    notifyAllObservers() {
        this.observers.forEach(observer => {
            observers.update()
        })
    }
    attach(observer) {
        this.observers.push(observer)
    }
}

// 观察者
class Observer {
    constructor(name, subject) {
        this.name = name
        this.subject = subject
        this.subject.attach(this)
    }
    update() {
        console.log(`${this.name} update, state is ${this.subject.getState()}`)
    }
}

// Test
let subject = new Subject()
let observer1 = new Observer('observer1',subject)
let observer2 = new Observer('observer2',subject)
let observer3 = new Observer('observer3',subject)
subject.setState(1)

/*
    observer1 update, state is 1
    observer2 update, state is 2
    observer3 update, state is 3
*/

场景

  • 网页事件绑定(先订阅事件,当点击时触发所有订阅好的事件)
<button id='btn1'>btn</button>

<script>
 $('#btn1').click(function(){
     console.log(1)
 })
 $('#btn1').click(function(){
     console.log(2)
 })
 $('#btn1').click(function(){
     console.log(3)
 })
</script>
  • Promise(Promise中每一个then函数都可以理解为是观察者,先订阅上,但不会立刻执行,然后当Promise状态变化resolved后,才执行)
var src = '/xxx'
var result = loadImg(src) // loadImg 为一个Promise
result.then(function(img){
    console.log('width',img.width)
    return img
}).then(function(img) {
    console.log('height',img.height)
})
  • jQuery callbacks、
var callbacks = $.Callbacks()   // 注意大小写
// 添加观察者
callbacks.add(function(info) {
    console.log('fn1',info)
})
callbacks.add(function(info) {
    console.log('fn2',info)
})
callbacks.add(function(info) {
    console.log('fn3',info)
})
callbacks.fire('gogogo')  // 所有观察者都会执行

/*
    fn1 gogogo
    fn2 gogogo
    fn3 gogogo
*/
  • nodeJs 自定义事件
const EventEmitter = require('events').EventEmitter

const emitter1 = new EventEmitter()
emitter1.on('some', () => {
    // 监听 some 事件
    console.log('some event is occured 1')
})
emitter1.on('some', () => {
    // 监听 some 事件
    console.log('some event is occured 2')
})
// 触发 some 事件
emitter1.emit('some')
// 继承 EvenEmitter
const EventEmitter = require('event').EventEmitter

// 任何构造函数都可以继承 EvenEmitter 的方法 on emit
class Dog extends EventEmitter {
    constructor(name) {
        super()
        this.name = name
    }
}
var simon = new Dog('simon')
simon.on('bark',function() {
    console.log(this.name, 'barked')
})
setInterval( () => {
    simon.emit('bark')
},1000)

其他场景

  • nodeJs 处理http请求;多进程通讯
  • vue / react 组件生命周期触发
  • vue watch (当监听的变量发生改变时,触发watch中绑定的事件,因此watch可以看做是观察者)
var vm = new Vue({
    el:'#app',
    data:{
        firstName: 'Foo',
        lastName: 'Bar',
        fullName: 'Foo Bar'
    },
    watch: {
        firstName: function(val) {
            this.fullName = val + ' ' + this.lastName
        },
        lastName: function(val) {
            this.fullName = this.firstName + ' ' + val
        }
    }
})
  • vue 双向绑定

设计原则验证

主题与观察者分离,不是主动触发而是被动监听,两者解耦,符合开放封闭原则


迭代器模式

介绍

  • 顺序访问一个集合
  • 使用者无需知道集合的内部结构(封装)

示例

// Jquery的一个例子

<p>jquery each</p>
<p>jquery each</p>
<p>jquery each</p>

<script>
    var arr = [1,2,3]
    var nodeList = document.getElementsByTagName('p')
    var $p = $('p')

    // 要对这三个变量进行遍历,需要写三个遍历方法
    // 第一  遍历arr
    arr.forEach(function(item){
        console.log(item)
    })
    // 第二  遍历 nodeList
    var i, length = nodeList.length
    for(i = 0; i < length; i++) {
        console.log(nodeList[i])
    }
    // 第三 遍历jquery对象
    $p.each(function(key, p) {
        console.log(key, p)
    })
</script>
// 写一个函数 能同时兼任这3种遍历方法

function each(data){
    // 既能遍历arr 又能遍历nodeList和Jquery对象
    var $data = $(data) // 生成迭代器
    $data.each(function(key, val) {
        console.log(key, val)
    })
}
each(arr)
each(nodeList)
each($p)


// 顺序遍历有序集合
// 使用者不必知道集合的内部结构

UML类图

代码演示

class Iterator {
    constructor(container) {
        this.list = container.list
        this.index = 0
    }
    next() {
        if(this.hasNext()) {
            return this.list[this.index++]
        }
    }
    hasNext() {
        if(this.index >= this.list.length) {
            return false
        }
        return true
    }
}

class Container {
    constructor(list) {
        this.list = list
    }
    // 生成迭代器
    getIterator() {
        return new Iterator(this)
    }
}

var arr = [1,2,3,4,5,6]
let container = new Container(arr)
let iterator = container.getIterator()
while(iterator.hasNext()){
    console.log(iterator.next())
}

/*
    1
    2
    3
    4
    5
    6
*/

场景

  • jQuery each
function each(data) {
    var $data = $(data)
    $data.each(function(key, p) {
        console.log(key, p)
    })
}
  • ES6 Iterator
    • ES6语法中,有序集合的数据类型已经有很多
    • Array Map Set String TypedArray arguments NodeList
    • 需要有一个统一的遍历接口来遍历所有数据类型
    • 注意:object不是有序集合,可以用Map代替
    • 以上数据类型,都有[Symbol.iterator]属性shun
    • 属性值是函数,执行函数返回一个迭代器
    • 这个迭代器就有next方法可顺序迭代子元素
    • 可运行Array.prototype[Symbol.iterator]来测试
function each(data) {
    // 生成遍历器
    let iterator = data[Symbol.iterator]()

    // console.log(iterator.next()) // 有数据时返回 { value:1,done: false }
    // console.log(iterator.next()) // 没有数据时返回 { value:undefined, done: true }
    let item = { done:false }
    while(!item.done) {
        item = iterator.next()
        if(!item.done) {
            console.log(item.value)
        }
    }
}

// Test
let arr = [1,2,3,4]
let nodeList = document.getElementByTagName('p')
let m = new Map()
m.set('a',100)
m.set('b',200)

each(arr)
each(nodeList)
each(m)
 // 'Symbol.iterator' 并不是人人都知道
 // 也不是每个人都需要封装一个each方法
 // 因此有了 'for...of' 语法   有[Symbol。iterator]才能执行 for...of

 function each(data) {
     for(let item of data) {
         console.log(item)
     }
 }

 each(arr)
 each(nodeList)
 each(m)

设计原则验证

迭代器对象与目标对象分离,即迭代器将使用者与目标对象隔离开,符合了开放封闭的原则


状态模式

介绍

  • 一个对象有状态变化
  • 每次状态变化都会触发一个逻辑
  • 不能总是用if...else来控制

示例

  • 交通信号灯不同颜色的变化

代码演示

// 状态( 红灯、绿灯、黄灯 )
class State {
    constructor(color) {
        this.color = color
    }
    // 设置状态
    handle(context) {
        console.log(`turn to ${this.color} light`)
        context.setState(this)
    }
}
// 主体
class Context {
    constructor() {
        this.state = null
    }
    // 获取状态
    getState() {
        return this.state
    }
    setState() {
        this.state = state
    }
}

let context = new Context()

let green = new State('green')
let yellow = new State('yellow')
let red = new State('red')

// 绿灯亮了
green.handle(context)
console.log(context.getState())

yellow.handle(context)
console.log(context.getState())

red.handle(context)
console.log(context.getState())

/*
    turn to green light
    State{ color: "green" }
    turn to yellow light
    State{ color: "yellow" }
    turn to red light
    State{ color: "red" } 
*/

场景

  • 有限状态机
    • 有限个状态,以及在这些状态之间的变化
    • 如交通信号灯
    • 例子:开源库 javascript-state-machine
// '收藏'和'取消'

// 状态机模型
var machine = new StateMachine({
    init: '收藏', // 初始状态,待收藏
    transitions: [
        {
            name: 'doStore',
            from: '收藏',
            to: '取消收藏'
        },
        {
            name: 'deleteStore',
            from: '取消收藏',
            to: '收藏'
        }
    ],
    methods: {
        // 执行收藏
        onDoStore: function(){
            alert('收藏成功')
            updateText()
        }
        // 取消收藏
        onDeleteStore: function(){
            alert('已取消收藏')
            updateText()
        }
    }
})

var $btn = $('#btn')

// 监听点击事件
$btn.click(function(){
    if(machine.is('收藏')){
        machine.doStore()
    }else{
        machine.deleteStore()
    }
})

// 更新文案
function updateText() {
    $btn.text(machine.state)
}

// 初始化文案
updateText()
  • 写一个简单的Promise
    • Promise就是一个有限状态机
    • Promise三种状态:pending、fullfilled、rejected
    • pending -> fullfilled 或者 pending -> rejected
    • Promise 不能逆向变化
// 定义状态机
var machine = new StateMachine({
    init: 'pending',
    
    transitions: [
        {
            name: 'resolve',
            from: 'pending',
            to: 'fullfilled'
        },
        {
            name: 'reject',
            from: 'pending',
            to: 'rejected'
        }
    ],
    methods: {
        // 成功
        onResolve: function(state, data) {
            // 参数:state - 当前状态; data - machine.resolve(xxx)执行时参入的参数
            data.successList.forEach( fn => fn() )
        },
        onReject: function(state, data) {
            // 参数:state - 当前状态; data - machine.resolve(xxx)执行时参入的参数
            data.failList.forEach( fn => fn() )
        }
    }
})
// 定义Promise
class MyPromise {
    constructor(fn) {
        this.successList = []
        this.failList = []

        fn( () => {
            // resolve函数
            machine.resolve(this)
        }, () => {
            // reject函数
            machine.reject(this)
        })
    }
    then(successFn,failFn) {
        this.successList.push(successFn)
        this.failList.push(failFn)
    }
}

设计原则验证

将状态对象和主题对象分离,状态的变化逻辑单独处理,符合开放封闭原则

总结

  • 设计模式一共有23种,本文介绍了前端常用的9种设计模式
  • 从生活上的事物作为例子,可以更好的理解设计模式的概念
  • 设计模式其实在我们的日常开发的经常出现,可以有的人在不知不觉中已经运用。
  • 将设计模式融入我们的日常代码风格中,可以让我们的编码更加优雅,减少冗余也提高复用性