资讯专栏INFORMATION COLUMN

JavaScript中常用的设计模式

NSFish / 2000人阅读

摘要:本文已同步到中常见的设计模式如果感觉写的还可以,就给个小星星吧,欢迎和收藏。本文中关于各种设计模式定义都是引用书中的,部分引用自百度百科已标出。下面把我整理出的常用设计模式按类型做个表格整理。

本文已同步到Github JavaScript中常见的设计模式,如果感觉写的还可以,就给个小星星吧,欢迎star和收藏。

最近拜读了曾探大神的《JavaScript设计模式与开发实践》,真是醍醐灌顶,犹如打通任督二脉的感觉,让我对JavaScript的理解加深了很多。

本文中关于各种设计模式定义都是引用书中的,部分引用自百度百科已标出。另外,本文中所举例子大多是书中的,自已做了一些修改和补充,用ES6(书中都是ES5的方式)的方式实现,以加深自己对“类”的理解,并不是自己来讲解设计模式,主要是做一些笔记以方便自己过后复习与加深理解,同时也希望把书中典型的例子整理出来和大家分享,共同探讨和进步。

一提起设计模式,相信大家都会脱口而出,23种设计模式,五大设计原则。这里就不说了,奈何我功力远远不够啊。下面把我整理出的常用JavaScript设计模式按类型做个表格整理。本文较长,如果阅读起来不方便,可链接到我的github中,多带带查看每一种设计模式。先整理这些,后续会继续补充,感兴趣的同学可以关注。

模式分类 名称
创建型 工厂模式
单例模式
原型模式
结构型 适配器模式
代理模式
行为型 策略模式
迭代器模式
观察者模式(发布-订阅模式)
命令模式
状态模式
创建型模式 工厂模式
工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象,用工厂方法代替new操作的一种模式。
class Creator {
    create(name) {
        return new Animal(name)
    }
}

class Animal {
    constructor(name) {
        this.name = name
    }
}

var creator = new Creator()

var duck = creator.create("Duck")
console.log(duck.name) // Duck

var chicken = creator.create("Chicken") 
console.log(chicken.name) // Chicken

小结:

构造函数和创建者分离,对new操作进行封装

符合开放封闭原则

单例模式

举一个书中登录框的例子,代码如下:





    




小结:

1.单例模式的主要思想就是,实例如果已经创建,则直接返回

function creatSingleton() {
    var obj = null
    // 实例如已经创建过,直接返回
    if (!obj) {
        obj = xxx
    }
    return obj
}

2.符合开放封闭原则

原型模式
用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。--百度百科

在JavaScript中,实现原型模式是在ECMAScript5中,提出的Object.create方法,使用现有的对象来提供新创建的对象的__proto__。

var prototype = {
    name: "Jack",
    getName: function() {
        return this.name
    }
}

var obj = Object.create(prototype, {
    job: {
        value: "IT"
    }
})

console.log(obj.getName())  // Jack
console.log(obj.job)  // IT
console.log(obj.__proto__ === prototype)  //true

更多关于prototype的知识可以看我之前的JavaScript中的面向对象、原型、原型链、继承,下面列一下关于prototype的一些使用方法

1. 方法继承

var Parent = function() {}
Parent.prototype.show = function() {}
var Child = function() {}

// Child继承Parent的所有原型方法
Child.prototype = new Parent()

2. 所有函数默认继承Object

var Foo = function() {}
console.log(Foo.prototype.__proto__ === Object.prototype) // true

3. Object.create

var proto = {a: 1}
var propertiesObject = {
    b: {
        value: 2
    }
}
var obj = Object.create(proto, propertiesObject)
console.log(obj.__proto__ === proto)  // true

4. isPrototypeOf

prototypeObj是否在obj的原型链上

prototypeObj.isPrototypeOf(obj)

5. instanceof

contructor.prototype是否出现在obj的原型链上

obj instanceof contructor

6. getPrototypeOf

Object.getPrototypeOf(obj) 方法返回指定对象obj的原型(内部[[Prototype]]属性的值)

Object.getPrototypeOf(obj)

7. setPrototypeOf

设置一个指定的对象的原型 ( 即, 内部[[Prototype]]属性)到另一个对象或 null

var obj = {}
var prototypeObj = {}
Object.setPrototypeOf(obj, prototypeObj)
console.log(obj.__proto__ === prototypeObj)  // true
结构型模式 适配器模式

举一个书中渲染地图的例子

class GooleMap {
    show() {
        console.log("渲染谷歌地图")
    }
}

class BaiduMap {
    show() {
        console.log("渲染百度地图")
    }
}

function render(map) {
    if (map.show instanceof Function) {
        map.show()
    }
}

render(new GooleMap())  // 渲染谷歌地图
render(new BaiduMap())  // 渲染百度地图

但是假如BaiduMap类的原型方法不叫show,而是叫display,这时候就可以使用适配器模式了,因为我们不能轻易的改变第三方的内容。在BaiduMap的基础上封装一层,对外暴露show方法。

class GooleMap {
    show() {
        console.log("渲染谷歌地图")
    }
}

class BaiduMap {
    display() {
        console.log("渲染百度地图")
    }
}


// 定义适配器类, 对BaiduMap类进行封装
class BaiduMapAdapter {
    show() {
        var baiduMap = new BaiduMap()
        return baiduMap.display() 
    }
}

function render(map) {
    if (map.show instanceof Function) {
        map.show()
    }
}

render(new GooleMap())         // 渲染谷歌地图
render(new BaiduMapAdapter())  // 渲染百度地图

小结:

适配器模式主要解决两个接口之间不匹配的问题,不会改变原有的接口,而是由一个对象对另一个对象的包装。

适配器模式符合开放封闭原则

代理模式

本文举一个使用代理对象加载图片的例子来理解代理模式,当网络不好的时候,图片的加载需要一段时间,这就会产生空白,影响用户体验,这时候我们可在图片真正加载完之前,使用一张loading占位图片,等图片真正加载完再给图片设置src属性。

class MyImage {
    constructor() {
        this.img = new Image()
        document.body.appendChild(this.img)
    }
    setSrc(src) {
        this.img.src = src
    }
}

class ProxyImage {
    constructor() {
        this.proxyImage = new Image()
    }

    setSrc(src) {
        let myImageObj = new MyImage()
        myImageObj.img.src = "file://xxx.png"  //为本地图片url
        this.proxyImage.src = src
        this.proxyImage.onload = function() {
            myImageObj.img.src = src
        }
    }
}

var proxyImage = new ProxyImage()
proxyImage.setSrc("http://xxx.png") //服务器资源url

本例中,本体类中有自己的setSrc方法,如果有一天网络速度已经不需要预加载了,我们可以直接使用本体对象的setSrc方法,,并且不需要改动本体类的代码,而且可以删除代理类。

// 依旧可以满足需求
var myImage = new MyImage()
myImage.setSrc("http://qiniu.sunzhaoye.com/CORS.png")

小结:

代理模式符合开放封闭原则

本体对象和代理对象拥有相同的方法,在用户看来并不知道请求的本体对象还是代理对象。

行为型模式 策略模式
定义一系列的算法,把它们一个个封装起来,并使它们可以替换
var fnA = function(val) {
    return val * 1
}

var fnB = function(val) {
    return val * 2
}

var fnC = function (val) {
    return val * 3
}


var calculate = function(fn, val) {
    return fn(val)
}

console.log(calculate(fnA, 100))// 100
console.log(calculate(fnB, 100))// 200
console.log(calculate(fnC, 100))// 300
迭代器模式

直接上代码, 实现一个简单的迭代器

class Creater {
    constructor(list) {
        this.list = list
    }

    // 创建一个迭代器,也叫遍历器
    createIterator() {
        return new Iterator(this)
    }
}

class Iterator {
    constructor(creater) {
        this.list = creater.list
        this.index = 0
    }

    // 判断是否遍历完数据
    isDone() {
        if (this.index >= this.list.length) {
            return true
        }
        return false
    }

    next() {
        return this.list[this.index++]
    }

}

var arr = [1, 2, 3, 4]

var creater = new Creater(arr)
var iterator = creater.createIterator()
console.log(iterator.list)  // [1, 2, 3, 4]
while (!iterator.isDone()) {
    console.log(iterator.next()) 
    // 1
    // 2
    // 3
    // 4
}

ES6中的迭代器:

JavaScript中的有序数据集合包括:

Array

Map

Set

String

typeArray

arguments

NodeList

注意: Object不是有序数据集合

以上有序数据集合都部署了Symbol.iterator属性,属性值为一个函数,执行这个函数,返回一个迭代器,迭代器部署了next方法,调用迭代器的next方法可以按顺序访问子元素

以数组为例测试一下,在浏览器控制台中打印测试如下:

var arr = [1, 2, 3, 4]

var iterator = arr[Symbol.iterator]()

console.log(iterator.next())  // {value: 1, done: false}
console.log(iterator.next())  // {value: 2, done: false}
console.log(iterator.next())  // {value: 3, done: false}
console.log(iterator.next())  // {value: 4, done: false}
console.log(iterator.next())  // {value: undefined, done: true}

小结:

JavaScript中的有序数据集合有Array,Map,Set,String,typeArray,arguments,NodeList,不包括Object

任何部署了[Symbol.iterator]接口的数据都可以使用for...of循环遍历

迭代器模式使目标对象和迭代器对象分离,符合开放封闭原则

观察者模式(订阅-发布模式)

先实现一个简单的发布-订阅模式,代码如下:

class Event {
    constructor() {
        this.eventTypeObj = {}
    }
    on(eventType, fn) {
        if (!this.eventTypeObj[eventType]) {
            // 按照不同的订阅事件类型,存储不同的订阅回调
            this.eventTypeObj[eventType] = []
        }
        this.eventTypeObj[eventType].push(fn)
    }
    emit() {
        // 可以理解为arguments借用shift方法
        var eventType = Array.prototype.shift.call(arguments)
        var eventList = this.eventTypeObj[eventType]
        for (var i = 0; i < eventList.length; i++) {
            eventList[i].apply(eventList[i], arguments)
        }
    }
    remove(eventType, fn) {
        // 如果使用remove方法,fn为函数名称,不能是匿名函数
        var eventTypeList = this.eventTypeObj[eventType]
        if (!eventTypeList) {
            // 如果没有被人订阅改事件,直接返回
            return false
        }
        if (!fn) {
            // 如果没有传入取消订阅的回调函数,则改订阅类型的事件全部取消
            eventTypeList && (eventTypeList.length = 0)
        } else {
            for (var i = 0; i < eventTypeList.length; i++) {
                if (eventTypeList[i] === fn) {
                    eventTypeList.splice(i, 1)
                    // 删除之后,i--保证下轮循环不会漏掉没有被遍历到的函数名
                    i--;
                }
            }
        }
    }
}
var handleFn = function(data) {
    console.log(data)
}
var event = new Event()
event.on("click", handleFn)
event.emit("click", "1")   // 1
event.remove("click", handleFn)
event.emit("click", "2")  // 不打印

以上代码可以满足先订阅后发布,但是如果先发布消息,后订阅就不满足了。这时候我们可以稍微修改一下即可满足先发布后订阅,在发布消息时,把事件缓存起来,等有订阅者时再执行。代码如下:

class Event {
    constructor() {
        this.eventTypeObj = {}
        this.cacheObj = {}
    }
    on(eventType, fn) {
        if (!this.eventTypeObj[eventType]) {
            // 按照不同的订阅事件类型,存储不同的订阅回调
            this.eventTypeObj[eventType] = []
        }
        this.eventTypeObj[eventType].push(fn)

        // 如果是先发布,则在订阅者订阅后,则根据发布后缓存的事件类型和参数,执行订阅者的回调
        if (this.cacheObj[eventType]) {
            var cacheList = this.cacheObj[eventType]
            for (var i = 0; i < cacheList.length; i++) {
                cacheList[i]()
            }
        }
    }
    emit() {
        // 可以理解为arguments借用shift方法
        var eventType = Array.prototype.shift.call(arguments)
        var args = arguments
        var that = this

        function cache() {
            if (that.eventTypeObj[eventType]) {
                var eventList = that.eventTypeObj[eventType]
                for (var i = 0; i < eventList.length; i++) {
                    eventList[i].apply(eventList[i], args)
                }
            }
        }
        if (!this.cacheObj[eventType]) {
            this.cacheObj[eventType] = []
        }

        // 如果先订阅,则直接订阅后发布
        cache(args)

        // 如果先发布后订阅,则把发布的事件类型与参数保存起来,等到有订阅后执行订阅
        this.cacheObj[eventType].push(cache)
    }
}

小结:

发布订阅模式可以使代码解耦,满足开放封闭原则

当过多的使用发布订阅模式,如果订阅消息始终都没有触发,则订阅者一直保存在内存中。

命令模式
--百度百科

在命令的发布者和接收者之间,定义一个命令对象,命令对象暴露出一个统一的接口给命令的发布者,而命令的发布者不用去管接收者是如何执行命令的,做到命令发布者和接收者的解耦。

举一个如果页面中有3个按钮,给不同按钮添加不同功能的例子,代码如下:




    
    cmd-demo


    
状态模式

举一个关于开关控制电灯的例子,电灯只有一个开关,第一次按下打开弱光,第二次按下打开强光,第三次按下关闭。





    
    state-demo



    
    


如果这时候需要增加一个超强光,则只需增加一个超强光的类,并添加pressBtn方法,改变强光状态下,点击开关需要把状态更改为超强光,超强光状态下,点击开关把状态改为关闭即可,其他代码都不需要改动。

class StrongLightState {
    constructor(light) {
        this.light = light
    }
    pressBtn() {
        this.light.setState(this.light.superLightState)
        console.log("开启超强光")
    }
}

class SuperLightState {
    constructor(light) {
        this.light = light
    }
    pressBtn() {
        this.light.setState(this.light.offLightState)
        console.log("关闭电灯")
    }
}

class Light {
    constructor() {
        this.offLightState = new OffLightState(this)
        this.weekLightState = new WeekLightState(this)
        this.strongLightState = new StrongLightState(this)
        this.superLightState = new SuperLightState(this)
        this.currentState = null
    }
    setState(newState) {
        this.currentState = newState
    }
    init() {
        this.currentState = this.offLightState
    }
}

小结:

通过定义不同的状态类,根据状态的改变而改变对象的行为,二不必把大量的逻辑都写在被操作对象的类中,而且容易增加新的状态

符合开放封闭原则

终于到最后可,历时多日地阅读与理解,并记录与整理笔记,目前整理出10中JavaScript中常见的设计模式,后续会对笔记继续整理,然后加以补充。由于笔者功力比较浅,如有问题,还望大家多多指正,谢谢。

参考文章:

JavaScript设计模式与开发实践
深入理解JavaScript系列/设计模式--汤姆大叔的博客
设计模式--菜鸟教程
JavaScript 中常见设计模式整理
ES6入门--阮一峰

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/100758.html

相关文章

  • ES6-7

    摘要:的翻译文档由的维护很多人说,阮老师已经有一本关于的书了入门,觉得看看这本书就足够了。前端的异步解决方案之和异步编程模式在前端开发过程中,显得越来越重要。为了让编程更美好,我们就需要引入来降低异步编程的复杂性。 JavaScript Promise 迷你书(中文版) 超详细介绍promise的gitbook,看完再不会promise...... 本书的目的是以目前还在制定中的ECMASc...

    mudiyouyou 评论0 收藏0
  • JavaScript常用设计模式

    摘要:原文链接常用设计模式设计模式设计模式是一种在长时间的经验与错误中总结出来可服用的解决方案。用来模拟接口的相关操作我很帅通过适配器函数来调用目的我很帅学习资料听飞狐聊设计模式系列设计模式汤姆大叔 原文链接: JavaScript常用设计模式 设计模式 设计模式是一种在长时间的经验与错误中总结出来可服用的解决方案。 设计模式主要分为3类: 创建型设计模式:专注于处理对象的创建 Const...

    RyanHoo 评论0 收藏0
  • +【13】JavaScript设计原则&&常用设计模式

    摘要:打个比方源码使用了模式,解决了问题,但是,在选择模式解决问题的背后又有多少思考 showImg(https://segmentfault.com/img/bVbupTE?w=480&h=260); 【前言】 最近阅读了《JavaScript设计模式与开发实践》,收获颇丰,于是想写一点总结及感想 showImg(https://segmentfault.com/img/bVbupUE?w...

    opengps 评论0 收藏0
  • JavaScript常用设计模式

    摘要:前言设计模式几十种,阅读了设计模式与开发实践这本书后,个人感觉就是围绕对象来设计的,发现日常写代码能用上的并不多,下面是常用的几种设计模式。前端服务端可以参考我的另一个仓库地址,一个简单的实时聊天参考来自设计模式与开发实践读书笔记 前言 设计模式几十种,阅读了《JavaScript设计模式与开发实践》这本书后,个人感觉js就是围绕对象来设计的,发现日常写代码能用上的并不多,下面是常用的...

    mengbo 评论0 收藏0
  • 前端每周清单半年盘点之 JavaScript

    摘要:前端每周清单专注前端领域内容,以对外文资料的搜集为主,帮助开发者了解一周前端热点分为新闻热点开发教程工程实践深度阅读开源项目巅峰人生等栏目。背后的故事本文是对于年之间世界发生的大事件的详细介绍,阐述了从提出到角力到流产的前世今生。 前端每周清单专注前端领域内容,以对外文资料的搜集为主,帮助开发者了解一周前端热点;分为新闻热点、开发教程、工程实践、深度阅读、开源项目、巅峰人生等栏目。欢迎...

    Vixb 评论0 收藏0
  • 学Java编程需要注意地方

    摘要:学编程真的不是一件容易的事不管你多喜欢或是多会编程,在学习和解决问题上总会碰到障碍。熟练掌握核心内容,特别是和多线程初步具备面向对象设计和编程的能力掌握基本的优化策略。   学Java编程真的不是一件容易的事,不管你多喜欢或是多会Java编程,在学习和解决问题上总会碰到障碍。工作的时间越久就越能明白这个道理。不过这倒是一个让人进步的机会,因为你要一直不断的学习才能很好的解决你面前的难题...

    leanxi 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<