原文出自於本人個人博客網站: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種:橋接模式、組合模式和享元模式