JavaScript设计模式(5)—— 结构型设计模式【1】

 

原文出自于本人个人博客网站:https://www.dzyong.com(欢迎访问)

转载请注明来源: 邓占勇的个人博客 - 《JavaScript设计模式(5)—— 结构型设计模式【1】

本文链接地址: https://www.dzyong.com/#/ViewArticle/90

设计模式系列博客:JavaScript设计模式系列目录(持续更新中)

 

结构型设计模式关注于如何将类或对象组合成更大、更复杂的结构,以简化设计。

结构型设计模式共7种,分为两次来进行介绍。本次介绍其中的外观模式适配器模式代理模式装饰者模式

 

外观模式

为一组复杂的子系统接口提供一个更高级的同一接口,通过这个接口使得对子系统接口的访问更容易。在JavaScript中有时也会对底层结构兼容性做出同一封装来简化用户使用。

例子:为document绑定一个click事件来实现隐藏提升抗的交互功能。

传统写法: 

document.onclick = function(e){
    e.preventDefault()
    if(e.target != document.getElementById('myinput'))
        hidePageAlert()
}
function hidePageAlert(){}

这样的写法存在两个问题,第一:如果其他人再次为document绑定事件时会覆盖掉原来的处理事件。第二:对于支持DOM2级事件的浏览器应该使用addEventListener方法,对于老版本的IE(9以下)应该使用attachEvent,对于不支持DOM2级的浏览器才使用onclick。

因此我们使用外观模式对绑定事件进行一个包装。

/*外观模式*/
let addEvent = function(dom, type, fn){
    //对于支持DOM2级时间处理程序addEventListener方法的浏览器
    if(dom.addEventListener)
        dom.addEventListener(type, fn, false)
    //对于不支持DOM2级时间处理程序addEventListener但支持attachEvent方法的浏览器
    else if(dom.attachEvent)
        dom.attachEvent('on' + type, fn)
    else
        dom['on' + type] = fn
}

在使用的时候就可以放心的绑定多个时间啦

let myInput = document.getElementById('myInput')
addEvent(myInput, 'click', function(){console.log('绑定了一个点击事件')})
addEvent(myInput, 'click', function(){console.log('绑定了两个点击事件')})

外观模式可以简化底层接口的复杂性,可以解决浏览器的兼容性问题,而在前面除了绑定事件外,在低版本的IE中也不兼容e.preventDefault()和e.target,这种也可以通过外观模式来解决。

//获取事件对象
let getEvent = function(event){
    //标准情况下返回event,IE下window.event
    return event || window.event
}
//获取元素
let getTarget = function(event){
    var event = getEvent(event)
    return event.target || event.srcElement
}
//阻止默认行为
let preventDefault = function(event){
    var event = getEvent(event)
    //标准浏览器
    if(event.preventDefault)
        event.preventDefault
    //IE浏览器
    else
        event.returnValue = false
}

addEvent(myInput, 'click', function(e){
    preventDefault(e)
    //获取时间源目标对象
    if(getTarget(e) !== document.getElementById('myInput'))
        console.log('绑定了三个点击事件')
})

上面只是外观模式应用的一部分,很多代码库都通过外观模式来进行封装多个功能,简化底层的操作方法。

let A = {
    //通过ID获取元素
    g: function(id){
        return document.getElementById(id)
    },
    css: function(id, key, value){
        document.getElementById(id).style[key] = value
    },
    attr: function(id, key, value){
        document.getElementById(id)[key] = value
    },
    html: function(id, html){
        document.getElementById(id).innerHTML = html
    },
    on: function(id, type, fn){
        //对于支持DOM2级时间处理程序addEventListener方法的浏览器
        if(dom.addEventListener)
            dom.addEventListener(type, fn, false)
        //对于不支持DOM2级时间处理程序addEventListener但支持attachEvent方法的浏览器
        else if(dom.attachEvent)
            dom.attachEvent('on' + type, fn)
        else
            dom['on' + type] = fn
    }
}

 

适配器模式

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

 例子:将自己开发的A框架与jQuery框架进行融合。(A框架与jQuery相似)

适配器的主要任务就是适配两种代码库中不兼容的代码。

window.A = A = jQuery

但是对于两个差别较大的框架适配起来就要麻烦许多了。如B框架的内容如下:

let B =  {}
B.g = function(id){
    return document.getElementById(id)
}
B.on = function(id, type, fn){
    var dom = typeof id === 'string' ? this.g(id) : id
    if(dom.addEventListener)
        dom.addEventListener(type, fn, false)
    else if(dom.attachEvent)
        dom.attachEvent('on' + type, fn)
    else
        dom['on' + type] = fn
}

B.on(window, 'load', function(){
    B.on('mybutton', 'click', function(){
        //do someing
    })
})

现在把jQuery融入到B框架中

B.g = function(id){
    return $(id).get(0)
}
B.on = function(id, type, fn){
    var dom = typeof id === 'string' ? $('#' + id) : $(id)
    dom.on(type, fn)
}

除此之外,适配器还有很多用途,比如某个方法需要传递很多参数,例如:

function doSomeThing(name, title, age, color, size, prize){}

我们要记住这些参数的顺序是很难的,因此我们通常以参数对象的形式进行传递

function doSomeThing(obj){}

即使是这样,还是存在一个弊端,我们无法判断参数的完整性,比如有些参数没有传入,有些参数存在默认值,此时我们通常的做法是用适配器来适配传入的这个参数对象。

let fun = function (obj) {
    var _adapter = {
        name: '',
        title: '',
        age: '',
        sex: ''
    }
    for (const key in _adapter) {
        _adapter[key] = obj[key] || _adapter[key]
    }
    //或者extend(_adapter, obj)

    //do things
}

适配器还可以用来对数据适配,在插件开发中会经常用到,比如这里有一个数组。

let data = ['js', 'book', '设计模式', '2013']

 这个数组中每个成员代表不同的意义,所以这种数据结构语义不友好,我们通常将其适配成对象的形式,如下面这种结构。

/*数组转对象的适配*/
let arrToObj = function (arr) {
    return{
        name: arr[0],
        type: arr[1],
        title: arr[2],
        data: arr[3]
    }
}

再讲一种情况,对于前后端交互中,后端返回的数据接口可能会经常改变,是不可控的,为了减少后期的麻烦,我们可以对后端返回的数据进行适配,如我们希望得到的是一个指定顺序的数组。

/*适配请求得到的数据*/
let ajaxAdapter = function(){
    //处理数据,按一定顺序返回数组
    return [data['key1'], data['key2'], data['key2']]
}

$.ajax({
    url: '',
    success: (res) =>{
        ajaxAdapter(res)
    }
})

 

代理模式

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

 跨域资源共享(CORS) 是一种机制,它使用额外的 HTTP 头来告诉浏览器  让运行在一个 origin (domain) 上的Web应用被准许访问来自不同源服务器上的指定的资源。当一个资源从与该资源本身所在的服务器不同的域、协议或端口请求一个资源时,资源会发起一个跨域 HTTP 请求。

对于跨域我们需要找一个代理来实现相互之间的通信。代理对象其实有很多,简单一点的如img之类的标签通过src属性可以向其他御下的服务器发送请求。不过这类请求是单项的,不会有响应数据

/*统计代理*/
let Count = (function () {
    //缓存图片
    var img  = new Image()
    //返回统计数据
    return function(param){
        //统计请求字符串
        var str = 'http://.***.com/a.gif?'
        //拼接请求字符串
        for (const i in param) {
            str += i + '=' + param[i]
        }
        //发送统计请求
        img.src = str
    }
})()
//测试用例,统计Num
Count({num:10})

第二种方式就是常常提到的JSONP,通过script标签,我们可以在script标签的src属性末尾加上我们的请求数据。

function jsopCallback(res){
    console.log(res)
}
//JSOP:在src后加入我们所需要传的数据信息,后台处理后返回
$.ajax({
    url: 'http://localhost:3000/',
    type: 'get',
    success: (res) =>{
        console.log(res)
    }
})
<script type="text/JavaScript" src="http://localhost:3000/?callback=jsopCallback"> </script>

callback参数的值为一个函数名,即返回后会调用该函数,在该函数中我们就可以得到所返回的数据。

 

装饰者模式

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

例子:用户信息表单需求有些变化,以前是用户点击输入框时,如果输入框输入内容有限制,那么其后面显示用户输入格式显示提示内容,现在需要多加一条,默认输入框上方有一文案,点击输入框时,文案消失。 

传统做法是在原来的点击事件处理函数中加入新增内容,如果这样的需求不止一个,我们需要挨个的去寻找对应事件的地方,并且破坏了原有的内容。对于这种情况我们可以使用装饰者模式,代码如下:

/*装饰者模式*/
let decorator = function(id, fn){
    //获取事件源
    var dom = document.getElementById(id)
    //若事件源已经绑定事件
    if(typeof dom.onclick === 'function'){
        //缓存原有的事件源原有的回调函数
        var oldClickFn  = dom.onclick
        //为事件源定义已新的事件
        dom.onclick = function () {
            //事件源原有回调函数
            oldClickFn()
            //执行新增的事件源函数
            fn()
        }
    }else{
        dom.onclick = fn()
    }
}


var name = document.getElementById('name')
var telwarn = document.getElementById('tel_warn_text')
var teldome = document.getElementById('tel_dome_text') 
name.onclick = function () {
    telwarn.style.display = 'none'
    // teldome.style.display = 'inline-block'   //新增
}
decorator('name', function(){
    teldome.style.display = 'inline-block'   //新增
})

可以看到,我们首先判断了是否存在原有事件,若存在,则先缓存原来的处理内容,重新绑定事件,并把之前缓存的内容放到新事件中,这样一来,我们在不破坏原基础上加入了新的内容,这就是装饰者模式。

 

 

下一期介绍结构型设计模式中剩下的3种:桥接模式、组合模式和享元模式

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

前端筱园

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值