36种JavaScript设计模式,代码演示

83 阅读30分钟

很多像我这样的菜鸟都不明白,设计模式到底多少种,每种是什么样子的,为便于回顾,花时间自己整合了一份,本文根据张容铭的《JavaScript设计模式》,以我自身理解简单整合出来,不易理解的地方我尽可能以我的理解白话解释,若理解有误之处还请纠正。如有较好的设计模式理论文章,可将地址留言给我,我添加到每项模式中,便于快速转攻理论。

创建型设计模式

简单工厂模式

又叫静态工厂方法,由一个工厂对象决定创建某一种产品对象类的实例,主要用来创建同一类对象。

function Factory(type) {
    this.name = type
}
const Java = new Factory('Java')
const UI = new Factory('UI')

工厂方法模式

通过对产品类的抽象使其创建业务主要负责用于创建多类产品类。

// 假设业务: 页面的广告资源投放,但是不同类目有不同的逻辑与样式

// 多个类目的生成,new的动作是重复的,整合起来
function Factory(type) {
    // 避免没有使用new关键字的情况
    if(this instanceof Factory) {
        const subject = new this[type](type)
        return subject
    } else {
        return new Factory(type)
    }
}
// 原型添加各个类目的构造函数,类目增减只需要原型上改变对应构造函数即可
Factory.prototype = {
    Java:function(name) {this.name=name},
    JavaScript:function(name) {this.name=name},
    PHP:function(name) {this.name=name},
    UI:function(name) {this.name=name}
}

const data = ['Java','JavaScript','PHP','UI']
data.forEach(item => console.log(Factory(item)))

抽象工厂模式

通过对类的工厂抽象使其业务用于产品类簇的创建,而不负责创建某一类产品的实例,即子类继承父类。

/**
 * 继承封装
 * @param {*} subType 子类
 * @param {*} superType 父类
 */
function Factory(subType,superType) {
    // 判断抽象工厂中是否有该抽象父类
    if(typeof Factory[superType] === 'function') {
        // 缓存父类原型
        const Fn = function() {}
        Fn.prototype = new Factory[superType]()
        // 改变子类subType指向
        subType.constructor = subType
        subType.prototype = new Fn()
    } else {
        throw new Error('该子类不存在')
    }
}

// 创建小汽车父类
Factory.Car = function() {this.type = 'car'}
    // 不便于子类使用的原型上的方法,当子类没有重写原型方法,报错提醒
Factory.Car.prototype = {
    getPrice() { return new Error('父类抽象方法不能调用') },
    getSpeed() { return new Error('父类抽象方法不能调用') }
}

// 创建红旗汽车子类
const HQ = function(price,speed) {
    this.price = price
    this.speed = speed
}

// 子父继承原型
Factory(HQ,'Car')
    // 改写父类原型上不便于子类使用的必要方法
HQ.prototype.getPrice = function() {return this.price}
HQ.prototype.getSpeed = function() {return this.speed}

// 实例测试
const H9 = new HQ(100,200)
console.log(H9.type) // car
console.log(H9.getPrice()) // 100
console.log(H9.getSpeed()) // 200

建造者模式

将一个复杂对象的构建层与其表示层相互分离,同样的构建过程可采用不同的表示。

// 假设业务:将找工作的人群简历向外推销的招聘信息发布业务

// 创建一个人
const Human = function(param = {}) {
    this.hobby = param.hobby || '保密'
    this.skill = param.skill || '保密'
}
Human.prototype = {
    getHobby:function() {return this.hobby},
    getSkill:function() {return this.skill}
}

// 创建姓名类
const Named = function(name) {
    this.name = name
}

// 创建职业类
const Work = function(work) {
    this.work = work
}

// 多个类组合创建一个具体的人
const Person = function(name,work) {
    // 创建一个人
    let _person = new Human()
    // 赋予名字
    _person.name = new Named(name)
    // 赋予期望职位
    _person.work = new Work(work)

    return _person
}

const person = new Person('lin','前端')
console.log(person) // {hobby: '保密',skill: '保密',name: { name: 'lin' }, work: { work: '前端' }}

原型模式

用原型实例指向创建对象的类,使用于创建新的对象的类共享原型对象上的属性及方法。继承原型的属性与方法

function prototypeExtend() {
    let Fn = function() {},
        args = arguments,
        i = 0
        for (; i < args.length; i++) {
            for (const key in args[i]) {
                Fn.prototype[key] = args[i][key]
            }
        }
        return new Fn()
}

// 实例测试
    // 需要继承的原型对象属性与方法 data
    const data = {
        name: 'lin',
        swim: function(speed) {this.speed = speed;console.log(this.speed)},
        run: function(speed) {this.speed = speed;console.log(this.speed)},
    }
    const prototypeData = prototypeExtend(data).__proto__
    console.log(prototypeData) // { name: 'lin', swim: [Function: swim], run: [Function: run] }

单例模式

又称为单体模式,是只允许实例化一次的对象类。

// 一个构造函数一生只能生成一个实例,new多少次都是同一个实例对象
const Person = (function() {
    class Person {
        constructor(name) {
            this.name = name
        }
    }
    let _instans = null
    return (...arg) => {
        if (!_instans)  _instans = new Person(...arg)
        return _instans
    }
})()
const p1 = Person('lin')
const p2 = Person('cai')
console.log(p1,p2); // lin lin
console.log(p1==p2); // true

结构型设计模式

外观模式

为一组复杂的子系统接口提供一个更高级的统一接口,通过这个接口使得对子系统接口的访问更容易。

// 外观一:方法封装,整合成库,便捷使用
const exterior = {
    getEl: function(id) {return document.getElementById(id)},
    css: function(id,key,val) {
        document.getElementById(id).style[key] = val
    },
    on: function(id,type,fn) {
        document.getElementById(id)['on' + type] = fn
    },
}
exterior.css('id','color','#fff') // 设置样式
exterior.on('id','click',()=>{}) // 绑定事件

// 外观二:兼容封装
function addEvent(dom,type,fn) {
    if(dom.addEventListener) {
        dom.addEventListener(type,fn,false)
    } else if(dom.attachEvent) {
        dom.attachEvent('on' + type,fn)
    } else {
        dom['on' + type] = fn
    }
}
const el = document.getElementById('el')
addEvent(el,click,()=>{})

适配器模式

将一个类(对象)的接口(方法或属性)转化成另一个接口,以满足用户需求,使类(对象)之间的接口的不兼容问题得以解决。

// 1-适配异类框架 需要两类框架相似
    // A框架写法
    var A = A || {}
    A.g = function(id) {return document.getElementById(id)}
    A.on = function(id,type,fn) {
        // ...
    }
    // 引入JQuery并适配
    A.g = function(id) { 
        return $(id).get(0)
    }
    A.on = function(id,type,fn) {
        return $('#' + id).on(type,fn)
    }
// 2-参数适配器
    // 默认参数  ES6可以直接使用默认值
    const obj = {
        name: 'lin',
        age:18
    }
    function doSomeThing(obj) {
        const _obj = {
            name: 'cai',
            age: 20,
            hobby: '唱歌'
        }
        for (const key in _obj) {
            _obj[key] = obj[key] || _obj[key]
        }
    }
    doSomeThing(obj)
// 3-数据适配
    const arr = ['lin', '18']
    function adapter(arr) {
        return {
            name: arr[0],
            age: arr[1]
        }
    }
    const data = adapter(arr)

代理模式

由于一个对象不能直接引用另一个对象,所以需要通过代理对象在这两个对象之间起到中介的作用。(暂无代码)

装饰者模式

在不改变原对象的基础上,通过对其进行包装拓展(添加属性或方法),使原有对象可以满足用户的更复杂需求。

// 封装方法,实现不改变原对象基础上增加fn事件
const decorator = function(id,fn) {
    // 获取事件源
    const input = document.getElementById(id)
    // 判断事件源是否绑定事件
    if(typeof input.onclick === 'function') {
        const onClickFn = input.onclick
        input.onclick = function() {
            // 原事件执行
            onClickFn()
            // 添加fn事件并执行
            fn()
        }
    } else {
        input.onclick = fn
    }
}
decorator(input,() => {console.log('我是新添加的事件')})

桥接模式

在系统沿着多个维度变化的同时,又不增加其复杂度并已达到解耦;主要特点:将实现层与抽象层解耦分离,使两部分可以独立变化。可能会增加开发成本,影响性能

// 多维变量类,好比多个零部件组合成一个完整件
// 运动类
function Speed(x,y) {
    this.x = x
    this.y = y
}
Speed.prototype.run = function() {
    console.log('跑步')
}
// 颜色类
function Color(cl) {
    this.color = cl
}
Color.prototype.draw = function() {
    console.log('上色')
}
// 说话类
function Speek(wd) {
    this.word = wd
}
Speek.prototype.say = function() {
    console.log('说话')
}

// 此时想创建一个人物类 可以运动说话, 除了创建人物类,也可以使用上面的类组合创建各种不同的类
function Person(x,y,wd) {
    this.speed = new Speed(x,y)
    this.speek = new Speek(wd)
}
Person.prototype.init = function() {
    this.speed.run()
    this.speek.say()
}
const person = new Person(2,4,'测试创建人')
console.log(person) //  { speed: { x: 2, y: 4 }, speek: { word: '测试创建人' } }

组合模式

又称部分-整体模式,将对象组合成树形结构以表示“部分整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性。

// 假设业务:页面添加一个新闻模块,内容根据用户平时关注的内容匹配,有的会是文字新闻,有的会是图片新闻等不同内容。
// 创建新闻父类
const News = function() {
    this.children = [] // 子组件容器
    this.element = null // 当前组件元素
}
// 错误跳出,避免子类未改写对应方法
News.prototype = {
    init: function() {throw new Error('请重写你的方法')},
    add: function() {throw new Error('请重写你的方法')},
    getElement: function() {throw new Error('请重写你的方法')},
}

// 创建容器类 结构  
const Container = function(id,parent) {
    News.call(this) // 继承父类
    this.id = id // 模拟ID
    this.parent = parent // 模块的父容器
    this.init() // 构建方法
}
// 继承
Container.prototype = new News()
Container.prototype.constructor = Container
// 改写方法
Container.prototype.init = function() {
    console.log('子类改写父类方法,初始化')
    this.element = document.createElement('div')
    this.element.id = this.id
    this.element.className = 'new-container'
}
Container.prototype.add = function(child) {
    console.log('子类改写父类方法,添加')
    this.children.push(child)
    this.element.appendChild(child.getElement())
    return this
}
Container.prototype.getElement = function() {
    console.log('子类改写父类方法,获取元素')
    return this.element
}
Container.prototype.show = function() {
    this.parent.appendChild(this.element)
}

// 创建图片新闻类 内容
const ImgNews = function(url = '',href = '#',className='') {
    News.call(this) // 继承父类
    this.url = url
    this.href = href
    this.className = className
    this.init() // 构建方法
}
// 继承
ImgNews.prototype = new News()
ImgNews.prototype.constructor = ImgNews
// 改写方法
ImgNews.prototype.init = function() {
    console.log('子类改写父类方法,初始化')
    this.element = document.createElement('a')
    let img = new Image()
    img.src = this.url
    this.element.className = 'img-new' + this.className
    this.element.href = this.href
}
ImgNews.prototype.add = function() {}
ImgNews.prototype.getElement = function() {
    console.log('子类改写父类方法,获取元素')
    return this.element
}

// 创建文字新闻类 内容
const IconNews = function(text = '',href = '#',type='video') {
    News.call(this) // 继承父类
    this.text = text
    this.href = href
    this.type = type
    this.init() // 构建方法
}
// 继承
IconNews.prototype = new News()
IconNews.prototype.constructor = IconNews
// 改写方法
IconNews.prototype.init = function() {
    console.log('子类改写父类方法,初始化')
    this.element.innerHTML = this.text
    this.element.className = 'icon-new' + this.type
    this.element.href = this.href
}
IconNews.prototype.add = function() {}
IconNews.prototype.getElement = function() {
    console.log('子类改写父类方法,获取元素')
    return this.element
}

// 创建实例
// 创建一个新闻容器模块
const news = new Container('news',document.body)
// 给容器模块内添加图片,文字  根据页面自由布局
news
.add(new ImgNews('img.png','#','img'))
.add(new IconNews('文字新闻','#','icon'))
.add(new IconNews('文字新闻2','#','icon2'))
// 也可以实现表单中不同的表单布局 创建表单容器模块,每行的布局及内容自由添加
// ...

享元模式

运用共享技术有效地支持大量的细粒度的对象,避免对象间拥有相同内容造成多余的开销。抽取公用的属性和方法进行封装,共享使用

// 假设业务:新闻翻页功能,点击下一页,之前5条隐藏,展示新的5条

// 业务封装:插入元素到页面并返回获取该元素的方法,将元素创建插入动作封装公用
const Flyweight = function() {
    let created = [] // 缓存已创建的元素
    // 创建新闻包装容器的函数
    const create = function () {
        const dom = document.createElement('div')
        // 添加元素
        document.getElementById('container').appendChild(dom)
        // 缓存元素并返回
        created.push(dom)
        return dom
    }
    return {
        getDiv: function() {
            // 如果已创建的元素小于当前页面的总个数,则创建一个并返回
            if(created.length < 5) {
                return create()
            } else { // 将数组靠前的元素移动到末尾
                const div = created.shift()
                created.push(div)
                return div
            }
        }
    }
}

// 业务实现,页面初始化
let arr = [1,2,3,4,5] // 新闻数据
let paper = 0, // 当前页码
    num = 5, // 每页显示的数量
    len = arr.length // 新闻数据长度
// 添加5条新闻
for (let i = 0; i < num; i++) {
    if(arr[i]) {
        Flyweight().getDiv().innerHTML = arr[i]
    }
}

// 绑定下一页事件
document.getElementById('next-btn').onclick = function() {
    if(arr.length < 5) return
    const n = ++paper * num % len // 获取当前页的第一条新闻的索引值
    for(let j = 0; j < num; j++) {
        if(arr[n + j]) {
            Flyweight().getDiv().innerHTML = arr[n + j]
        } else if(arr[n + j - len]) { // 当前项的索引超出了数据长度
            Flyweight().getDiv().innerHTML = arr[n + j - len]
        } else {
            Flyweight().getDiv().innerHTML = ''
        }
    }
}

行为型设计模式

模板方法模式

父类中定义一组操作算法骨架,而将一些实现步骤延迟到子类中,使得子类可以不改变父类的算法结构的同时,可重新定义算法中某些实现步骤。

// 假设业务:项目中出现各种各样的提示框,现将提示框样式统一化。

// 创建一个基础的模板类 实现一个基础的提示框组件
const Alert = function(data) {
    // 没有数据时退出函数,有点简陋,就表示个意思,具体判断还是需要各种分析
    if(!data) return
    this.content = data.content // 设置内容
    this.panel = document.createElement('div') // 创建提示框容器元素
    this.contentNode = document.createElement('p') // 创建提示内容元素
    this.confirmBtn = document.createElement('span') // 创建确定按钮元素
    this.closeBtn = document.createElement('b') // 创建关闭按钮元素
    this.paner.className = 'alert' // 添加类名
    this.confirmBtn.className = 'confirm-btn' // 添加类名
    this.closeBtn.className = 'close-btn' // 添加类名
    this.confirmBtn.innerHTML = data.confirm || '确定'
    this.closeBtn.innerHTML = data.close || '关闭'
    this.contentNode.innerHTML = this.content // 提示内容填充
    this.success = data.success || function() {} // 点击确定的回调
    this.fail = data.fail || function() {} // 点击取消的回调
}
Alert.prototype = {
    // 初始化
    init: function() {
        this.panel.appendChild(this.closeBtn)
        this.panel.appendChild(this.contentNode)
        this.panel.appendChild(this.confirmBtn)
        document.body.appendChild(this.panel)
        this.bindEvent() // 事件绑定
        this.show() // 显示提示框
    },
    // 事件绑定
    bindEvent: function() {
        const _this = this
        this.closeBtn.onclick = function() {
            _this.fail()
            _this.hide()
        }
        this.confirmBtn.onclick = function() {
            _this.success()
            _this.hide()
        }
    },
    hide:function() {
        this.panel.style.display = 'none'
    },
    show:function() {
        this.panel.style.display = 'block'
    }
}

// 如此,接下来只需要继承基础的模板类,再自行改写对应方法,实现自己所需要的提示框组件
// 实现标题提示框
const TitleAlert = function(data) {
    Alert.call(this,data)
    this.title = data.title
    this.titleNode = document.createElement('h3')
    this.titleNode.innerHTML = this.title
}
TitleAlert.prototype = new Alert()
TitleAlert.prototype.init = function() {
    // 插入元素
    this.panel.insertBefore(this.titleNode,this.panel.firstChild)
    // 继承基础模板类的init方法
    Alert.prototype.init.call(this)
}
// 还可以给基础提示框增加各种想要的样式 元素等。。。

观察者模式

又称作发布-订阅模式或消息机制,定义了一种依赖关系,解决主体对象与观察者之间功能的耦合。关于观察者与发布订阅是否同一类,网上说法不一,书中作者认为是同一类,我心中也无定论,将两种情况都列出来,根据实际情况使用

// 监控:当被观察者触发了一些条件的时候,观察者触发事件
// 监控对象状态,一旦变化,立即触发

// 观察者构造函数
class Observer {
    constructor(fn = () => {}) {
        // 被观察者状态改变触发观察者方法
        this.fn = fn
    }
}
// 被观察者构造函数
class BeObserver {
    constructor(state) {
        this.state = state
        this.observers = []
    }
    // 改变状态
    setState(state) {
        this.state = state
        this.observers.forEach(item => item.fn(state))
    }
    // 添加观察者
    addObserver(obs) {
        this.observers.push(obs)
    }
    // 删除观察者
    delObserver(obs) {
        this.observers = this.observers.filter(item => item !== obs)
    }
}

发布订阅模式

class Observer {
    constructor() {
        this.massage = {} // 存储订阅过的信息及方法
    }
    // 添加订阅信息及方法
    on(type,fn) {
        if(typeof fn !== 'function') throw new Error('fn is not a function')
        if(!this.massage[type]) this.massage[type] = []
        this.massage[type].push(fn)
    }
    // 删除订阅信息及方法
    off(type,fn) {
        if(!fn) {
            delete this.massage[type]
            return
        }
        if(!this.massage[type]) return
        this.massage[type] = this.massage[type].filter(item => item !== fn)
    }
    // 触发订阅信息的方法
    emit(type) {
        if(!this.massage[type]) return
        this.massage[type].forEach(item => { item() });
    }
}

状态模式

当一个对象内部状态发生改变时,会导致其行为的改变,这看起来像是彻底改变了对象。类似策略模式,匹配多种动作连续执行使用状态模式,匹配单一动作执行使用策略模式

// 假设:超级玛丽游戏
// 要吃蘑菇,就要跳起顶石砖;要过悬崖,就要跳起;要避免被乌龟咬到,就要开枪打掉;要躲避导弹,就要蹲下
// 创建一个状态类
const MarryState = function() {
    let _currenState = {} // 内部状态私有变量对象
    
    // 状态配置
    let states = {
        jump: function() {console.log('跳跃')},
        move: function() {console.log('移动')},
        shoot: function() {console.log('射击')},
        squat: function() {console.log('蹲下')},
    }
    
    // 动作控制
    let Action = {
        // 改变状态
        changeState:function() {
            const args = Array.from(arguments)
            _currenState = {} // 重置状态
            if(args.length) {
                // 有动作控制时,往内部状态中缓存
                args.forEach((item,index) => {
                    _currenState[args[index]] = true
                })
            }
            return this // 链式调用
        },
        // 执行动作
        goes:function() {
            for (const key in _currenState) {
                states[key] && states[key]()
            }
            return this // 链式调用
        }
    }
    
    return {
        changeState:Action.changeState,
        goes:Action.goes
    }
}

// 实例测试
const marry = new MarryState()
marry
.changeState('jump','shoot') // 设计动作:跳跃 射击
.goes() // 执行一次跳跃 射击
.changeState('move')
.goes() // 执行一次移动
.goes() // 执行一次移动

策略模式

将定义的一组算法封装起来,使其相互之间可以替换。封装的算法具有一定的独立性,不会随客户端变化而变化。

// 一个问题匹配多个解决方案
const strategy = (function() {
    const strategyData = {
        '方案一': function(...agruments) {console.log('解决方案')},
        '方案二': function(...agruments) {console.log('解决方案')},
        '方案三': function(...agruments) {console.log('解决方案')},
    }
    function test(key) {
        // ...
        strategyData(key)() // 根据key匹配对应的值并执行
    }
    test.add = function(...agruments) {console.log('给strategyData对象添加方案')}
    test.del = function(...agruments) {console.log('给strategyData对象删除方案')}

    return test
})()

职责链模式

解决请求的发送者与请求的接受者之间的耦合,通过职责链上的多个对象对分解请求流程,实现请求在多个对象之间的传递,直到最后一个对象完成请求的处理。流程分割,化整为零,各个流程单独封装,低耦合

// 假设业务:用户信息提交表单校验交互体验的需求,但后期有可能对需求进行修改--半成品需求。
// 分析:表单需求流程:事件源,异步请求,创建组件;将流程进行分解成单个流程。

// 1.封装异步请求模块--即发送请求获取数据(就是封装ajax,此处不做代码演示)
// 2.响应数据适配模块--即对响应得到的数据进行处理
/**
 * @param {*} data 响应数据
 * @param {*} dealType 响应数据处理对象
 * @param {*} dom 事件源
 */
const dealData = function(data,dealType,dom) {
    // 数据类型
    let type = Object.prototype.toString.call(data)
    switch (dealType) {
        // 提示信息组件
        case 'sug':
            let newData = null
            // 数据为数组 创建提示框组件
            if(type === '[object Array]') {newData = data}
            // 对象数据转成数组数据
            else if(type === '[object Object]') {
                const arr = []
                for (const key in data) {
                    arr.push(data[key])
                }
                newData = arr
            } else {
                // 其他数据类型的数据
                newData = [data]
            }
            return createSug(newData,dom) // (函数在下面创建)
            break;
            // 校验信息组件
        case 'validate':
            return createValidate(data,dom) // (函数在下面创建)
            break
    }
}
// 3.创建组件模块--即将处理后的响应数据代入到页面中
// 创建提示框组件
function createSug(data,dom) {
    // 提示框的内容赋值并显示,代码就不演示了....
}
// 创建校验组件
function createValidate(data,dom) {
    // 校验结果赋值并显示...
}
// 4.单元测试--即对上面封装的方法各自单独测试 得到预期结果即可。如此当需求变更,那么修改成本大大降低。

命令模式

将请求与实现解耦并封装成独立对象,从而使不同的请求对客户端的实现参数化。将各种动作复杂的逻辑都封装成一个指令,通过传递的参数来执行不同的动作

// 类似vue的指令
const CanvasCommand = (function () {
    const canvas = document.getElementById('canvas')
    ctx = canvas.getContext('2d')
    // 内部方法封装成各个命令
    const Action = {
        // 填充颜色
        fillStyle: function (c) { ctx.fillStyle = c },
        // 填充矩阵
        fillRect: function (x,y,width,height) { ctx.fillRect(x, y, width, height) },
        // 描边色彩
        strokeStyle: function (c) { ctx.strokeStyle = c },
        // 描边矩形
        strokeRect: function (x,y,width,height) { ctx.strokeRect(x, y, width, height) },
        // 填充字体
        fillText: function (text,x,y) { ctx.fillText(text, x, y) },
        // 开启路径
        beginPath: function () { ctx.beginPath() },
        // 移动画笔触点
        moveTo: function (x,y) { ctx.moveTo(x,y) },
        // 画笔连线
        lineTo: function (x,y) { ctx.lineTo(x,y) },
        // 绘制弧线
        arc: function (x,y,r,begin,end,dir) { ctx.arc(x,y,r,begin,end,dir) },
        // 填充
        fill: function () { ctx.fill() },
        // 描边
        stroke: function() { ctx.stroke() }
    }
    return {
        // 命令接口
        excute: function(msg) {
            if(!msg) return
            // msg是一个数组时,遍历执行命令
            if(msg.length) {
                msg.forEach(item => {
                    arguments.callee(item)
                })
            } else { // msg不是数组,是个对象时
                // 转为数组
                msg.param = Object.prototype.toString.call(msg.param) === '[object Array]' ? msg.param : [msg.param]
                // Action内部可能会有调用this,防止出错,改变下this指向
                Action[msg.command].apply(Action,msg.param)
            }
        }
    }
})()
// 需要执行的命令的数组
const msg = [
    {
        command:'fillStyle',
        param: 'red'
    },
    {
        command:'fillRect',
        param: [20,20,100,100]
    },
]
// 调用命令
CanvasCommand.excute(msg)

访问者模式

针对于对象结构中的元素,定义在不改变该对象的前提下访问结构中的元素。通过封装,让某一种数据结构可以使用另外一种数据结构独有的方法,如下例,对象可以使用数组的方法

// 访问者模式解决数据与数据的操作方法之间的耦合,将数据的操作方法独立于数据,使其可以自由演变,
// 因此,访问者模式更适合用于那些数据稳定但操作方法易变的环境。

// 创建一个访问器
const Visitor = (function () {
    return {
        // 截取
        splice: function() {
            const args = [].splice.call(arguments,1)
            // arguments[0]是对象 访问器实例
            return [].splice.apply(arguments[0], args)
        },
        // 添加元素
        push: function() {
            const len = arguments[0].length || 0
            const args = this.splice(arguments, 1)
            arguments[0].length = len + arguments.length - 1
            return [].push.apply(arguments[0], args)
        },
        // 弹出最后一次添加的元素
        pop: function() {
            return [].pop.apply(arguments[0])
        },
    }
})()

// 测试
const a = {}
Visitor.push(a,1,2,3)
console.log(a) // { '0': 1, '1': 2, '2': 3, length: 3 }
const key = Visitor.pop(a)
console.log(key) // 3
console.log(a) // { '0': 1, '1': 2, length: 3 }

中介者模式

通过中介者对象封装一系列对象之间的交互,使对象之间不再相互引用,降低他们之间的耦合。

// 假设业务:用户自由设置导航模块样式,类似QQ空间装扮

// 创建中介者对象
const Mediator = (function () {
    const msg = {} // 消息对象
    return {

        /** 订阅消息--用户选中的导航模块存储起来
         * @param {*} type 消息名称
         * @param {*} action 回调函数
         */
        register: function(type,action) {
            // 订阅消息不存在,则初始化
            if(!msg[type]) msg[type] = []
            msg[type].push(action)
        },

        /** 发送消息--将缓存的对应键名的回调全部执行
         * @param {*} type 消息名称
         */
        send: function(type) {
            if(!msg[type]) return
            // 订阅的消息回调全部执行--用户勾选的模块,改变css让缓存中的模块展示或隐藏
            msg[type].forEach(item => {
                item && item()
            })
        }
    }
})()
// 用户缓存需要的导航模块
Mediator.register('dom',() => console.log(1))
Mediator.register('dom',() => console.log(2))
// 完成设置后发送消息,将需要显示的模块展示,不需要的模块隐藏
Mediator.send('dom') // 顺序打印  1  2

备忘录模式

在不破坏对象的封装性的前提下,在对象之外捕获并保存该对象内部的状态以便日后对象使用或者对象恢复到以前的某个状态。就两个字:缓存

假设业务:用户点击下一页获取过数据后,再点击上一页,不需要再次发送请求,直接缓存中取数据。此模式就不作代码演示。

迭代器模式

在不暴露对象内部结构的同时,可以顺序地访问聚合对象内部的元素。内部封装方法,定义一个指针对象,通过修改指针来访问数组对应位置的数据

// 示例采用了阮一峰的ES6模拟代码,顺便一提,ES6中的迭代器Iterator主要供for...of消费。
// 优化循环语句,使代码清晰易读
const MyIterator = function(arr) {
    let nextIndex = 0
    return {
        next: function() { // 指针指向下一个数组元素
            return nextIndex < arr.length ? 
            {value:arr[nextIndex++],done:false} : 
            {value:undefined,done:true}
        },
        first: function() {}, // 指针指向第一个数组元素
        last: function() {}, // 指针指向最后一个数组元素
        // 还可以实现其他方法,根据自身需求...
    }
}

解释器模式

对于一种语言,给出其文法表示形式,并定义一种解释器,通过使用解释器来解释语言中定义的句子。这个模式书中的例子直接操作DOM,代码太复杂不易理解,我在掘金上参考的例子,使用场景并不广,为什么这么说呢,因为我不知道要用在哪,哈哈哈

class Context {
    constructor() {
      this._list = []; // 存放 终结符表达式
      this._sum = 0; // 存放 非终结符表达式(运算结果)
    }
    // 获取运算结果
    get sum() {
      return this._sum;
    }
    // 设置运算值
    set sum(newValue) {
      this._sum = newValue;
    }
    // 添加项
    add(expression) {
      this._list.push(expression);
    }
    // 获取数组集合
    get list() {
      return [...this._list];
    }
  }
  // 创建一个加法类
  class PlusExpression {
     // 解释器方法 sum++
    interpret(context) {
      if (!(context instanceof Context)) {
        throw new Error("TypeError");
      }
      context.sum = ++context.sum;
    }
  }
  // 创建一个运算减法类
  class MinusExpression {
    // 解释器方法 sum--
    interpret(context) {
      if (!(context instanceof Context)) {
        throw new Error("TypeError");
      }
      context.sum = --context.sum;
    }
  }
  
  /** 以下是测试代码 **/
  const context = new Context();
  
  // 依次添加: 加法 | 加法 | 减法 的表达式
  context.add(new PlusExpression());
  context.add(new PlusExpression());
  context.add(new MinusExpression());
  
  // 依次执行: 加法 | 加法 | 减法 的表达式
  context.list.forEach(expression => expression.interpret(context));
  console.log(context.sum);

技巧型设计模式

链模式

通过在对象方法中将当前对象返回,实现对同一个对象多个方法的链式调用。从而简化对该对象的多个方法的多次调用,对该对象的多次引用。即链式调用,JQuery:你直接点我名吧

// 假设业务:模拟JQ获取元素,链式调用

// 创建A类
const A = function(selector) {
    return new A.fn.init(selector)
}
// A绑定原型到fn属性上
A.fn = A.prototype = {
    constructor: A,
    // 初始化函数
    init:function(selector) {
        // 一系列操作逻辑
        this[0] = document.getElementById(selector)
        this.length = 1
        // 最终返回this,为链式调用
        return this
    },
    length: 2,
    // size方法没有返回this,调用size方法后链就断了
    size: function() {
        return this.length
    },
}

A.fn.init.prototype = A.fn

// 测试
console.log(A('demo').size()) // 1

委托模式

多个对象接收并处理同一请求,他们将请求委托给另一个对象统一处理请求。事件委托都知道的吧?冒泡总该知道的吧?此模式代码也是没有的

// 好吧,假装有代码,来个典型的事件委托
从前有个大哥ul标签,ul底下有许多个小弟li标签,每个li小弟经常被人打,
li小弟被打一次就会冒泡上去,让ul大哥知道是哪个小弟li挨打,
ul大哥仗义,替代将要被打的那个li小弟挨这顿打,这顿打就从小弟li转移给了ul大哥。
总结:统一由大哥挨打,小弟只管惹事。

// 好吧,再来一个,书上认为下面这例子是委托模式的一种,但我感觉是策略模式。

// 普通做法,多次请求不同的数据
$.get(url + '?data=a',function (res) {})
$.get(url + '?data=b',function (res) {})
$.get(url + '?data=c',function (res) {})
$.get(url + '?data=d',function (res) {})

// 委托给另一个对象进行数据分发
const data = {
    a: function() {},
    b: function() {},
    c: function() {},
    d: function() {},
}
$.get(url,function (res) {
    for (const key in res) {
        data[key] && data[key](res[key])
    }
})

数据访问对象模式

抽象和封装对数据源的访问与存储,DAO通过对数据源链接的管理方便对数据的访问与存储。这句话看着是否感觉到拗口?演示代码有点长,我简单描述下:其实就是模拟后端接口,在前端对数据库的CRUD操作进行封装,演示代码是将浏览器的本地存储当作数据库,也可以连接SQL、mongoDB等数据库;是不是感觉就是在写接口了呢。

// 假设业务:新品上线,引导用户使用功能的说明,在前端做存储数据

// 封装一个本地数据库 存储在localStorage中
/**
 * DAO对象类
 * @param {*} preId 本地存储数据库前缀名
 * @param {*} timeSign 时间戳与存储数据(value)之间的拼接符
 */
const BaseLocalStorage = function(preId,timeSign) {
    this.preId = preId
    this.timeSign = timeSign || '-'
}
// 原型方法定义
BaseLocalStorage.prototype = {
    // 保存操作状态
    status: {
        success: 0, // 成功
        error: 1, // 失败
        overflow: 2, // 溢出
        timeout: 3 // 过期
    },
    // 保存本地存储链接
    storage: localStorage || window.localStorage,
    // 获取本地数据库key
    getKey: function(key) {
        return this.preId + key
    },
    /**
     * 增删改查操作
     * @param {*} key 存储的键
     * @param {*} value 存储的值
     * @param {*} callback 回调函数
     * @param {*} time 设置过期时间戳
     */
    set: function(key,value,callback,time) {
        let status = this.status.success
        const Key = this.getKey(key) // 获取库中真实的key名
        // 这里传入的参数time有可能是错误的时间格式,捕捉错误并获取默认时间
        try {
            time = new Date(time).getTime() || time.getTime()
        } catch (e) {
            time = new Date().getTime() + 1000*60*60*24*31
        }

        try {
            const val = time + this.timeSign + value // 1677824038569-false
            this.storage.setItem(Key, val)
        } catch (e) {
            // 溢出失败,返回状态
            status = this.status.overflow
        }
        // 回调函数执行并将status、Key和value传入
        callback && callback.call(this, status, Key, value)
    },
    get: function(key,callback) {
        let status = this.status.success,
            Key = this.getKey(key),
            value = null, // 默认值为空
            timeLen = this.timeSign.length,
            that = this,
            index, // 时间戳与存储数据之间的拼接符的起始位置
            time, // 时间戳
            result // 最终获取的数据
            try {
                value = this.storage.getItem(Key)
            } catch (e) {
                // 获取失败则将状态返回
                result = {
                    status:this.status.error,
                    value: null
                }
                callback && callback.call(this, result)
                return result
            }
            // 数据如果获取成功
            if(value) {
                index = value.indexOf(that.timeSign)
                time = +value.slice(0,index) // 获取时间戳
                // 时间未过期
                if(new Date(time).getTime() > new Date().getTime() || time == 0) {
                    value = value.slice(index + timeLen)
                } else { // 时间过期了
                    value = null
                    status = this.status.timeout
                    that.remove(Key)
                }
            } else { // 数据获取失败
                status = this.status.error
            }
            // 数据返回
            result = {
                status,
                value
            }
            callback && callback.call(this, result)
            return result
    },
    remove: function(key,callback) {
        let status = this.status.error,
            Key = this.getKey(key),
            value = null

            try {
                value = this.storage.getItem(Key)
            } catch (e) {}

            if(value) {
                this.storage.removeItem(Key)
                status = this.status.success
            }
            // 状态成功 则截取拼接符后的数据,否则返回null
            const result = status == 0 ? value.slice(value.indexOf(this.timeSign) + this.timeSign.length) : null
            // 将删除的数据返回
            callback && callback.call(this , status, result)
            return {
                status,
                result
            }
    }
}

// 测试
const LS = new BaseLocalStorage('LS__')
LS.set('lin','18岁',function(){console.log(arguments)}) // [0, 'LS__lin', '18岁']
LS.get('lin') // {status: 0, value: '18岁'}
LS.remove('lin') // {status: 0, value: '18岁'}

节流模式

对重复的业务逻辑进行节流控制,执行最后一次操作并取消其他操作,以提高性能。这个是不是感觉很熟悉?没错,就是防抖节流的节流

理论:JavaScript专题之跟着 underscore 学节流

/**
 * 默认进入第一次立即执行,结束后回调执行一次
 * @param {*} fn 
 * @param {*} wait 间隔时间
 * @param {object} options 配置项 二者只可设置其一
 *                         disableFirst:false 表示禁止第一次的立即执行
 *                         disableLast:false 表示禁止最后一次的回调触发
 * @returns 
 */
function throttle(fn,wait = 1000,options = {}) {
    let timeout,context,args
    let prev = 0 // 记录上次执行的时间
    let throttled = function () {
        // 时间戳方法 实现第一次立即执行
        let nowTime = +new Date()
        if(options.disableFirst === false && !prev) prev = nowTime

        let remaining = wait - (nowTime - prev)
        context = this
        args= arguments
        // 在间隔时间外立即执行,间隔时间内延迟执行
        if(remaining <= 0 || remaining > wait) {
            if(timeout) {
                clearTimeout(timeout)
                timeout = null
            }
            prev = nowTime
            fn.apply(context,args)
        } else if(!timeout && options.disableLast !== false) {
            timeout = setTimeout(() => {
                prev = options.disableFirst === false ? 0 : +new Date()
                timeout = null
                fn.apply(context,args)
            }, remaining)
        }
    }
    return throttled
}

简单模板模式

通过格式化字符串拼凑出视图,避免创建视图时大量的节点操作。优化内存开销。想了半天白话,最终:这个没什么好说的,看看代码就懂了。

// 用数据对象{demo:"this is a demo"}去格式化 <a>{#demo#}</a> 字符串模板,
// 得到 <a>this is a demo</a> 渲染后的模板,插入到页面中,减少对DOM的直接操作。

const A= {}

// 模板渲染方法--塞内容
A.formateString = function(str, data) {
    return str.replace(/\{#(\w+)#\}/g, (match,key) => data[key] ?? '')
}

// 模板生成器--建标签模板
A.view = function(name) {
    const v = {
        // 代码模板
        code: '<pre><code>{#code#}</code></pre>}',
        // 图片模板
        img: '<img src="{#src#}" alt="" title="{#title#}" />',
        // 带有Id和类的模板
        part: '<div id="{#id#}" class="{#class#}">{#part#}</div>',
        // 组合模板
        theme: [
            '<div>',
                '<h1>{#title#}</h1>',
                '{#content#}',
            '</div>'
        ].join(',')
        // ....
    }
    if(Array.isArray(name)) {
        let tpl = ''
        name.forEach(item => {
            // arguments.callee 用来引用当前正在执行的函数体 此时正在执行的函数体是A.view
            tpl += arguments.callee(item) // arguments.callee(item) === A.view(item)
        })
        return tpl
    } else {
        return v[name] || (`<${name}>{#${name}#}</${name}>`)
    }
}

// 测试

// 生成标签模板
const tpl = A.view(['h2','p','ul'])
console.log(tpl) // <h2>{#h2#}</h2><p>{#p#}</p><ul>{#ul#}</ul>

// 往标签模板中添加内容
const Tep = A.formateString(tpl,{h2: '我是h2标签',p: '我是p标签',ul: '我是ul标签'})
console.log(Tep) // <h2>我是h2标签</h2><p>我是p标签</p><ul>我是ul标签</ul>

// 如果要往标签内添加标签
const liTep = A.formateString(A.view('li'), { li: A.view(['strong','span']) })
console.log(liTep) // <li><strong>{#strong#}</strong><span>{#span#}</span></li>

惰性模式

减少每次代码执行时的重复性的分支判断,通过对对象重定义来屏蔽原对象中的分支判断。改写方法减少分支,根据实际情况使用,应用还是挺广的

const A = {}

// 传统的浏览器兼容,每次执行都需要判断分支
A.on = function(dom,type,fn) {
    if(dom.addEvenListener) {
        dom.addEvenListener(type,fn,false)
    } else if(dom.attachEvent) {
        dom.attachEvent('on' + type,fn)
    } else {
        dom['on' + type] = fn
    }
}
// 惰性模式优化---1.通过闭包方式改写A.on方法,后续调用A.on方法就不再需要分支判断
A.on = function(dom,type,fn) {
    if(dom.addEvenListener) {
        return function(dom,type,fn) {
            dom.addEvenListener(type,fn,false)
        }
    } else if(dom.attachEvent) {
        return function(dom,type,fn) {
            dom.attachEvent('on' + type,fn)
        }
    } else {
        return function(dom,type,fn) {
            dom['on' + type] = fn
        }
    }
}
// 惰性模式优化---2.直接改写A.on方法,后续调用A.on方法就不再需要分支判断
A.on = function(dom,type,fn) {
    if(dom.addEvenListener) {
        A.on = function(dom,type,fn) {
            dom.addEvenListener(type,fn,false)
        }
    } else if(dom.attachEvent) {
        A.on = function(dom,type,fn) {
            dom.attachEvent('on' + type,fn)
        }
    } else {
        A.on = function(dom,type,fn) {
            dom['on' + type] = fn
        }
    }
}

参与者模式

在特定的作用域中执行给定的函数,并将参数原封不动的传递。此模式实际是函数绑定(apply、bind、call)和函数柯里化的结晶

理论:异国战场——参与者模式

/**
 * 假设业务:点击一个按钮时将额外的数据传入这个事件的回调中
 * @param {*} fn 执行函数
 * @param {*} context 上下文环境
 * @returns 
 */
function Bind(fn,context) {
    const args = [].slice.call(arguments,2)
    return function() {
        const addArgs = [].slice.call(arguments)
        return fn.apply(context,[...args,...addArgs])
    }
}

等待者模式

通过对多个异步进程监听,来触发未来发生的动作。

// 等待者类---等待所有异步都成功结束
const Waiter = function() {
    let dfd = [], // 注册了的等待对象容器
        doneArr = [], // 成功回调方法容器
        failArr = [], // 失败回调方法容器
        that = this
    // 监控对象类
    const Promise = function() {
        this.resolved = false
        this.rejected = false
    }
    Promise.prototype = {
        resolve: function() {
            this.resolved = true
            // 如果没有监控对象则取消执行
            if(!dfd.length) return
            let i = dfd.length
            for(--i; i >= 0; i--) {
                // 监控对象任意一个失败则返回
                if(dfd[i] && !dfd[i].resolved || dfd[i].rejected) return
                dfd.splice(i,1)
            }
            _exec(doneArr)
        },
        reject: function() {
            this.rejected = true
            // 如果没有监控对象则取消执行
            if(!dfd.length) return
            // 清空
            dfd.splice(0)
            _exec(failArr)
        }
    }
    // 回调执行方法
    function _exec(arr) {
        arr.forEach(item => {
            item && item()
        })
    }
    // 创建一个Promise监控对象
    that.Deferred = function() { return new Promise() }
    // 添加监控对象 参数:监控对象
    that.when = function() {
        dfd = [].slice.call(arguments)
        dfd.forEach((item,index) => {
            // 如果不存在监控对象、监控对象已经成功结束、监控对象已经失败结束、不是监控对象,则剔除
            if(!item || item.resolved || item.rejected || !item instanceof Promise) {
                dfd.splice(index,1)
            }
        })
        return that // 链式调用
    }
    // 向成功回调容器添加方法
    that.done = function() {
        doneArr = [...doneArr, ...arguments]
        return that
    }
    // 向失败回调容器添加方法
    that.fail = function() {
        failArr = [...failArr,...arguments]
        return that
    }
}

// 测试
const waiter = new Waiter()

const first = function() {
    const dtd = waiter.Deferred()
    setTimeout(() => {
        console.log('first')
        dtd.resolve()
    }, 5000)
    return dtd
}()

const second = function() {
    const dtd = waiter.Deferred()
    setTimeout(() => {
        console.log('second')
        dtd.resolve()
    }, 10000)
    return dtd
}()

// 添加监控对象
waiter.when(first,second)
      // 添加成功与失败回调
      .done(function(){console.log('success')})
      .fail(function(){console.log('fail')})
// 结果:5秒后打印first 10秒后打印second 最后才打印success

架构型设计模式

此类型的模式代码可以很明确的感受到一件事情,我懒散了,白话释义没了,注释逐渐变少,实测更是没有。

同步模块模式

将复杂的系统分解成高内聚、低耦合的模块,使系统开发变得可控、可维护、可拓展,提高模块的复用率。

// 假设情况:同一份代码,前端A要修改导航栏提示信息,前端B正在修改导航栏,
// 若同时修改,代码提交会冲突,是否只能等待B结束后A再修改呢?
// 使用模块化开发解决问题

const F = {} // 定义模块管理器单体对象
/**
 * 添加模块方法(理论上应该在闭包中实现,可以隐蔽内部信息,此处为演示看的清楚,忽略此步骤)
 * @param {*} str 模块路由名称
 * @param {*} fn 模块方法
 * @returns 
 */
F.define = function(str,fn) {
    // 解析模块路由
    let parts = str.split('.'),
        // old当前模块的祖父模块,parent当前模块的父级模块
        // 如果在闭包中,为了屏蔽对模块直接访问,建议将模块添加给闭包内部私有变量。
        old = parent = this,
        // i--模块层级,len--模块层级长度
        i = len = 0
    // 如果第一个模块是模块管理器单体对象,则移除
    if(parts[0] === 'F') {
        parts = parts.slice(1)
    }
    // 屏蔽define和module两个模块方法的重写
    if(parts[0] === 'define' || parts[0] === 'module') return
    // 遍历路由模块并定义每层模块
    for(len = parts.length;i < len; i++) {
        // 如果模块不存在,添加模块
        if(typeof parent[parts[i]] === 'undefined') parent[parts[i]] = {}
        // 缓存下一层级祖父模块
        old = parent
        // 缓存下一层级父模块
        parent = parent[parts[i]]
    }
    // 此时的i等于parts.length 故--
    if(fn) old[parts[--i]] = fn()
    // 返回模块管理器单体对象
    return this
}
// 简单使用define
F.define('string',() => {
    return {
        trim: () => {}
    }
})
F.define('dom',()=>{}) // 创建方法
F.string.trim() // 调用方法
F.dom() // 调用方法

/**
 * 模块调用方法(注意:调用的方法通常为已经创建的模块对象)
 */
F.module = function() {
    // 参数转数组
    let args = [].slice.call(arguments),
          // 回调函数
          fn = args.pop(),
          // 获取依赖模块
          parts = args[0] && Array.isArray(args[0]) ? args[0] : args,
          // 依赖模块列表
          modules = [],
          // 模块路由
          modIDs = '',
          // 依赖模块索引
          i = 0,
          // 依赖模块长度
          ilen = parts.length,
          // 父模块,模块路由层级索引,长度
          parent,
          j,
          jlen
    // 遍历依赖模块
    while (i < ilen) {
        if(typeof parts[i] === 'string') { // 如果是路由
            // 设置当前模块父对象F
            parent = this
            // 解析模块路由并屏蔽父对象
            modIDs = parts[i].replace(/^F\./,'').split('.')
            // 遍历
            for(j = 0, jlen = modIDs.length; j < jlen;j++) {
                // 重置父模块
                parent = parent[modIDs[j]] || false
            }
            modules.push(parent)
        } else { // 如果是对象
            modules.push(parts[i])
        }
        i++
    }
    fn.apply(null.modules)
}
// 简单使用module
F.module('dom', 'string.trim',()=>{})

异步模块模式

将复杂的系统分解成高内聚、低耦合的模块,使系统开发变得可控、可维护、可拓展,提高模块复用率。

// 对于加载中的文件,同步模块获取不到,此时就需要异步模块
(function(F) {
    /**
     * 创建或调用模块方法
     * @param {*} url 模块url
     * @param {*} modDeps 依赖模块
     * @param {*} modCallback 模块回调
     */
    F.module = function(url,modDeps,modCallback) {
        // 参数定义
        let args = [].slice.call(arguments),
            // 获取回调
            callback = args.pop(),
            // 获取依赖模块
            deps = (args.length && Array.isArray(args[args.length-1])) ? args.pop() : [],
            // 获取模块路由
            url = args.length ? args.pop() : null,
            // 依赖模块序列
            params = [],
            // 未加载的依赖模块数量统计
            depsCount = 0,
            i = 0,
            len
        if(len = deps.length) {
            while(i < len) {
                // 闭包保存i
                (function(i) {
                    // 未加载模块统计
                    depsCount++
                    // 异步加载依赖模块---方法在下方定义
                    loadModule(deps[i],function(mod) {
                        // 依赖模块序列中添加依赖模块接口引用
                        params[i] = mod
                        // 依赖加载完成后--
                        depsCount--
                        // 依赖全部加载完
                        if(depsCount === 0) {
                            // 在模块缓存器中矫正该模块,并执行构造函数
                            setModule(url,params,callback) // 方法在下方定义
                        }
                    })
                })(i)
                i++
            }
        } else { // 无依赖模块
            setModule(url,[],callback)
        }
    }
    let moduleCache = []
    /**
     * 设置模块并执行模块构造函数
     * @param {*} moduleName 模块id名称
     * @param {*} params 依赖模块
     * @param {*} callback 构造函数
     */
    const setModule = function(moduleName,params,callback) {
        let _module,fn
        if(moduleCache[moduleName]) {
            _module = moduleCache[moduleName]
            _module.status = 'loaded'
            _module.exports = callback ? callback.apply(_module,params) : null
            while(fn = _module.onload.shift()) {
                fn(_module.exports)
            }
        } else {
            callback && callback.apply(null,params)
        }
    }
    /**
     * 异步加载依赖模块所在文件
     * @param {*} moduleName 模块路径(id)
     * @param {*} callback 模块加载完成回调函数
     */
    const loadModule = function(moduleName,callback) {
        let _module // 依赖模块
        if(moduleCache[moduleName]) { // 如果依赖被要求加载过
            _module = moduleCache[moduleName]
            // 如果模块加载完成
            if(_module.status === 'loaded') {
                // 执行模块加载完成回调函数
                setTimeout(callback(_module.exports), 0)
            } else {
                // 缓存该模块所处文件加载完成回调函数
                _module.onload.push(callback)
            }
        // 模块第一次被依赖引用
        } else {
            // 缓存该模块初始化信息
            moduleCache[moduleName] = {
                moduleName:moduleName, // 模块ID
                status: 'loading',// 模块对应文件加载状态
                exports: null, // 模块接口
                onload:[callback] // 模块对应文件加载完成回调函数缓冲器
            }
            // 加载模块对应文件
            loadScript(getUrl(moduleName)) // 两种方法下方定义
        }

    }
    // 获取文件路径
    const getUrl = function(moduleName) {
        return String(moduleName).replace(/\.js$/g,'') + '.js'
    }
    // 加载脚本文件
    const loadScript = function(src) {
        const _script = document.createElement('script')
        _script.type = 'text/JavaScript'
        _script.charset = 'UTF-8'
        _script.async = true
        _script.src = src
        document.getElementsByTagName('head')[0].appendChild(_script)
    }
    
})(function() { return window.F = {} }())

Widget模式

Web Widget指的是一块可以在任意页面中执行的代码块。Widget模式是指借用Web Widget思想将页面分解成部件,针对部件开发,最终组合成完整的页面。也算是模块化开发吧,架构型设计的模式代码都不好码,能理解不懂表达,书中内容也偏多,光靠代码可能不太好理解

// 分四步:1--建立模板引擎  2--获取模板  3--处理模板  4--模板编译
// 同步异步模块模式已经完成了module方法,这边直接拿来使用
F.module('lib/template', function(template) {
          /**
           * 模板引擎 处理数据和编译模板入口
           * @param str 模板容器ID或者模板字符串
           * @param data 渲染数据
           */
    const _TplEngine = function(str,data) {
              if(Array.isArray(data)) {
                  let html = ''
                  for(let i = 0;i < data.length;i++) {
                      html += _getTpl(str)(data[i])
                  }
                  return html
              } else {
                  return _getTpl(str)(data)
              }
          },
          // 获取模板
          _getTpl = function(str) {
            const el = document.getElementById(str)
            if(el) {
                let html = /^(textarea|input)$/i.test(el.nodeName) ? el.value : el.innerHTML
                return _compileTpl(html)
            } else {
                return _compileTpl(str)
            }
          },
          // 处理模板 
          // 假设要处理的模板 <a>{%=test%}</a> 处理成 template_array.push('<a>',typeof(test) === "undefined"?"":test,'<a/>')
          _dealTpl = function() {
            let _left = '{%',_right = '%}'
            return String(str)
                   .replace(/&lt/g,'<')
                   .replace(/&gt/g,'>')
                   .replace(/[\r\t\n]/g,'')
                   .replace(new RegExp(_left + '=(.*?)' + _right, 'g') , ",typeof($1) === 'undefined'?'':$1,")
                   .replace(new RegExp(_left, 'g') , "')")
                   .replace(new RegExp(_right, 'g') , "template_array.push('")
          },
          // 编译执行 str--模板数据
          _compileTpl = function(str) {
            const fnBody = `
                var template_array=[];\n
                var fn = (function(data){\n
                    var template_key = ''\n
                    for(key in data) { \n
                        template_key += ('var '+key+'=data[\"'+key+'\"]')\n
                    }\n
                    evel(template_key)\n
                    template_array.push('"+_dealTpl(str)+"')\n
                    template_key = null\n
                })(templateData)\n
                fn=null\n
                return template_array.join('')
            `
            return new Function('templateData',fnBody)
          }

    return _TplEngine
})

MVC模式

MVC:模型(Model)— 视图(View)— 控制器(Controller),用一种将业务逻辑、数据、视图分离的方式组织架构代码。 1678522064217.jpg

// 假设业务:如上图,鼠标移入模块弹出,移出模块缩回,点击下方箭头显隐
(function() {
    let MVC = {}
    
    // 模型 -- 实现数据的增删改查
    MVC.model = function() {
        let M = {} // 内部数据对象
        // 服务器获取的数据,此处简化,直接使用同步数据,就写一个数据吧,简化一下
        M.data = {
            // 侧边导航栏模块
            slideBar: [
                {
                    text: '萌妹子',
                    icon: 'left_meng.png',
                    title: '喵耳萝莉的千本樱',
                    content: '自古幼女有三好',
                    img: 'left_meng_img.png',
                    href: 'https://baidu.com'
                }
            ],
            // 新增其他模块等等追加代码。。。
            news: []
        }
        // 配置数据,页面加载时即提供
        M.config = {
            // 配置导航动画配置数据
            slideBarCloseAnimate: false
            // 其他效果配置等等。。。
        }
        // 返回数据模型层对像操作方法
        return {
            // 获取缓存的数据
            getData: (m) => M.data[m],
            getConfig: (c) => M.config[c],
            // 设置服务器获取过来的数据,更新数据
            setData: function(m,v) {
                M.data[m] = v
                return this
            },
            setConfig: function(c,v) {
                M.config[c] = v
                return this
            }
        }
    }()
    
    // 视图
    MVC.view = function() {
        let M = MVC.model
        // 内部视图方法对象
        let V = {
            // 创建侧边导航模块视图
            createSlideBar: function() {
                let html = ''
                let data = M.getData('slideBar')
                if(!data || !data.length) return
                // 根据模型对象方法获取过来的数据,使用原生js画页面操作,此处代码省略。。。
            },
            // 新增其他模块等等追加代码。。。
            createNews: function() {}
        }
        // 获取视图接口方法
        return (v) => V[v]()
    }()
    
    // 控制器
    MVC.ctrl = function() {
        let M = MVC.model
        let V = MVC.view
        let C = {
            // 侧边导航栏模块
            initSlideBar: function() {
                // 页面渲染视图
                V('createSlideBar')
                // 使用模型对象方法(M)获取数据和配置,控制页面的动画效果等一系列操作,太长了,代码省略。。。
            },
            // 新增其他模块等等追加代码。。。
            initNews: function() {}
        }
        // 为导航栏添加交互效果等
        for (const key in C) {
            C[key] && C[key]()
        }
    }()
})()

MVP模式

此MVP非彼MVP哈。MVP:模型(Model)— 视图(View)— 管理器(Presenter),V层不直接引用M层内的数据,而是通过P层实现对M层内的数据访问,即所有层次的交互都发生在层中。代码不知道怎么整理,太太太长了,我想着你们也不会看,代码省略,省略,嘿嘿

// 假装有代码。
(function(window) {
    let MVP = function() {}
    MVP.model = function() {}()
    MVP.view = function() {}()
    MVP.presenter = function() {}()
    // MVP入口
    MVP.init = function() {}
    window.MVP = MVP
})(window)
console.log(window.MVP)

MVVM模式

MVVM:模型(Model)— 视图(View)— 视图模型(ViewModel),为V量身定做一套VM,并在VM中创建属性和方法,为V绑定M数据并实现交互。

// 假设业务:创建一个滚动条和进度条
// <div class='slider' data-bind="type:'slider',data:demo1"></div> 一行代码实现滑动组件
(function(window) {
    // 获取页面字体大小,作为创建页面的UI尺寸参照物
    const fontSize = function() {
        const size = document.body.currentStyle ? 
                     document.body.currentStyle['fontSize'] :
                     getComputedStyle(document.body,false)['fontSize']
        return parseInt(size) // 取整
    }()

    const VM = function() {
        const Method = {
            // 进度条组件创建方法,画页画代码省略。。。
            progressbar: function(dom,data) {},
            // 滑动条组件创建方法,画页画代码省略。。。
            slider: function(dom,data) {}
        }
        // 获取元素自定义数据的方法
        function getBindData() {}

        // 组件实例化方法
        return function() {
            // 获取页面所有元素
            const doms = document.body.getElementsByTagName('*')
            let ctx = null // 元素自定义属性数据
            for(let i = 0; i < doms.length;i++) {
                ctx = getBindData(doms[i]) // 获取元素自定义属性数据
                ctx.type && Method[ctx.type] && Method[ctx.type](doms[i],ctx)
            }
        }
    }
    window.VM = VM

    window.onload = function() {
        VM()
    }
})(window)
// <div class='slider' data-bind="type:'slider',data:demo1"></div>

MVC、MVP和MVVM模式之间的区别与联系

设计模式是一种思想,本文中的代码只是方便理解每种设计模式的思想,并非标准代码,这下面试官问你知道有哪些设计模式的时候,知道该怎么回答了吗?各位升职加薪发大财是我的美好祝愿。