代理模式:為一個對象提供一個代用品或占位符蒲每,以便控制它的訪問叨咖。
當(dāng)我們不方便直接訪問某個對象時,或不滿足需求時嘿期,可考慮使用一個替身對象來控制該對象的訪問。替身對象可對請求預(yù)先進行處理埋合,再決定是否轉(zhuǎn)交給本體對象备徐。
生活小栗子:
- 代購;
- 明星經(jīng)紀(jì)人甚颂;
- 和諧上網(wǎng)
經(jīng)常 shopping 的同學(xué)蜜猾,對代購應(yīng)該不陌生。自己不方便直接購買或買不到某件商品時振诬,會選擇委托給第三方蹭睡,讓代購或黃牛去做購買動作。程序世界的代理者也是如此赶么,我們不直接操作原有對象肩豁,而是委托代理者去進行。代理者的作用辫呻,就是對我們的請求預(yù)先進行處理或轉(zhuǎn)接給實際對象清钥。
模式特點
- 代理對象可預(yù)先處理請求,再決定是否轉(zhuǎn)交給本體放闺;
- 代理和本體對外顯示接口保持一致性
- 代理對象僅對本體做一次包裝
模式細分
- 虛擬代理(將開銷大的運算延遲到需要時執(zhí)行)
- 緩存代理(為開銷大的運算結(jié)果提供緩存)
- 保護代理(黑白雙簧祟昭,代理充當(dāng)黑臉,攔截非分要求)
- 防火墻代理(控制網(wǎng)絡(luò)資源的訪問)
- 遠程代理(為一個對象在不同的地址控件提供局部代表)
- 智能引用代理(訪問對象執(zhí)行一些附加操作)
- 寫時復(fù)制代理(延遲對象復(fù)制過程怖侦,對象需要真正修改時才進行)
JavaScript 中常用的代理模式為 “虛擬代理” 和 “緩存代理”篡悟。
模式實現(xiàn)
實現(xiàn)方式:創(chuàng)建一個代理對象谜叹,代理對象可預(yù)先對請求進行處理,再決定是否轉(zhuǎn)交給本體搬葬,代理和本體對外接口保持一致性(接口名相同)荷腊。
// 例子:代理接聽電話,實現(xiàn)攔截黑名單
var backPhoneList = ['189XXXXX140']; // 黑名單列表
// 代理
var ProxyAcceptPhone = function(phone) {
// 預(yù)處理
console.log('電話正在接入...');
if (backPhoneList.includes(phone)) {
// 屏蔽
console.log('屏蔽黑名單電話');
} else {
// 轉(zhuǎn)接
AcceptPhone.call(this, phone);
}
}
// 本體
var AcceptPhone = function(phone) {
console.log('接聽電話:', phone);
};
// 外部調(diào)用代理
ProxyAcceptPhone('189XXXXX140');
ProxyAcceptPhone('189XXXXX141');
代理并不會改變本體對象急凰,遵循 “單一職責(zé)原則”停局,即 “自掃門前雪,各找各家”香府。不同對象承擔(dān)獨立職責(zé)董栽,不過于緊密耦合,具體執(zhí)行功能還是本體對象企孩,只是引入代理可以選擇性地預(yù)先處理請求锭碳。例如上述代碼中,我們向 “接聽電話功能” 本體添加了一個屏蔽黑名單的功能(保護代理)勿璃,預(yù)先處理電話接入請求擒抛。
虛擬代理(延遲執(zhí)行)
虛擬代理的目的,是將開銷大的運算延遲到需要時再執(zhí)行补疑。
虛擬代理在圖片預(yù)加載的應(yīng)用歧沪,代碼例子來至 《JavaScript 設(shè)計模式與開發(fā)實踐》
// 本體
var myImage = (function(){
var imgNode = document.createElement('img');
document.body.appendChild(imgNode);
return {
setSrc: function(src) {
imgNode.src = src;
}
}
})();
// 代理
var proxyImage = (function(){
var img = new Image;
img.onload = function() {
myImage.setSrc(this.src); // 圖片加載完設(shè)置真實圖片src
}
return {
setSrc: function(src) {
myImage.setSrc('./loading.gif'); // 預(yù)先設(shè)置圖片src為loading圖
img.src = src;
}
}
})();
// 外部調(diào)用
proxyImage.setSrc('./product.png'); // 有l(wèi)oading圖的圖片預(yù)加載效果
緩存代理(暫時存儲)
緩存代理的目的,是為一些開銷大的運算結(jié)果提供暫時存儲莲组,以便下次調(diào)用時诊胞,參數(shù)與結(jié)果不變情況下,從緩存返回結(jié)果锹杈,而不是重新進行本體運算撵孤,減少本體調(diào)用次數(shù)。
應(yīng)用緩存代理的本體竭望,要求運算函數(shù)應(yīng)是一個純函數(shù)邪码,簡單理解比如一個求和函數(shù) sum
, 輸入?yún)?shù) (1, 1)
, 得到的結(jié)果應(yīng)該永遠是 2
咬清。
純函數(shù):固定的輸入闭专,有固定的輸出,不影響外部數(shù)據(jù)旧烧。
模擬場景:60道判斷題測試影钉,每三道題計分一次,根據(jù)計分篩選下一步的三道題目?
三道判斷題得分結(jié)果:
- (0, 0 ,0)
- (0, 0, 1)
- (0, 1, 0)
- (0, 1, 1)
- (1, 0, 0)
- (1, 0, 1)
- (1, 1, 0)
- (1, 1, 1)
總共七種計分結(jié)果粪滤。60/3 = 20
斧拍,共進行 20 次計分,每次計分執(zhí)行 3 個循環(huán)累計杖小,共 60 個循環(huán)肆汹。接下來,借用 “緩存代理” 方式予权,來實現(xiàn)最少本體運算次數(shù)昂勉。
// 本體:對三道題答案進行計分
var countScore = function(ansList) {
let [a, b, c] = ansList;
return a + b + c;
}
// 代理:對計分請求預(yù)先處理
var proxyCountScore = (function() {
var existScore = {}; // 設(shè)定存儲對象
return function(ansList) {
var attr = ansList.join(','); // eg. ['0,0,0']
if (existScore[attr] != null) {
// 從內(nèi)存返回
return existScore[attr];
} else {
// 內(nèi)存不存在,轉(zhuǎn)交本體計算并存入內(nèi)存
return existScore[attr] = countScore(ansList);
}
}
})();
// 調(diào)用計分
proxyCountScore([0,1,0]);
60 道題目扫腺,每 3 道題一次計分岗照,共 20 次計分運算,但總的計分結(jié)果只有 7 種笆环,那么實際上本體 countScore()
最多只需運算 7 次攒至,即可囊括所有計算結(jié)果。
通過緩存代理的方式躁劣,對計分結(jié)果進行臨時存儲迫吐。用答案字符串組成屬性名 ['0,1,0']
作為 key
值檢索內(nèi)存,若存在直接從內(nèi)存返回账忘,減少包含復(fù)雜運算的本體被調(diào)用的次數(shù)志膀。之后如果我們的題目增加至 90 道, 120 道鳖擒,150 道題時溉浙,本體 countScore()
運算次數(shù)仍舊保持 7 次,中間節(jié)省了復(fù)雜運算的開銷蒋荚。
ES6 的 Proxy
ES6新增的 Proxy
代理對象的操作戳稽,具體的實現(xiàn)方式是在 handler
上定義對象自定義方法集合,以便預(yù)先管控對象的操作期升。
ES6 的 Proxy語法:let proxyObj = new Proxy(target, handler);
- target: 本體广鳍,要代理的對象
- handler: 自定義操作方法集合
- proxyObj: 返回的代理對象,擁有本體的方法吓妆,不過會被
handler
預(yù)處理
// ES6的Proxy
let Person = {
name: '以樂之名'
};
const ProxyPerson = new Proxy(Person, {
get(target, key, value) {
if (key != 'age') {
return target[key];
} else {
return '保密'
}
},
set(target, key, value) {
if (key === 'rate') {
target[key] = value === 'A' ? '推薦' : '待提高'
}
}
})
console.log(ProxyPerson.name); // '以樂之名'
console.log(ProxyPerson.age); // '保密'
ProxyPerson.rate = 'A';
console.log(ProxyPerson.rate); // '推薦'
ProxyPerson.rate = 'B';
console.log(ProxyPerson.rate); // '待提高'
handler
除常用的 set/get
赊时,總共支持 13 種方法:
handler.getPrototypeOf()
// 在讀取代理對象的原型時觸發(fā)該操作,比如在執(zhí)行 Object.getPrototypeOf(proxy) 時
handler.setPrototypeOf()
// 在設(shè)置代理對象的原型時觸發(fā)該操作行拢,比如在執(zhí)行 Object.setPrototypeOf(proxy, null) 時
handler.isExtensible()
// 在判斷一個代理對象是否是可擴展時觸發(fā)該操作祖秒,比如在執(zhí)行 Object.isExtensible(proxy) 時
handler.preventExtensions()
// 在讓一個代理對象不可擴展時觸發(fā)該操作,比如在執(zhí)行 Object.preventExtensions(proxy) 時
handler.getOwnPropertyDescriptor()
// 在獲取代理對象某個屬性的屬性描述時觸發(fā)該操作舟奠,比如在執(zhí)行 Object.getOwnPropertyDescriptor(proxy, "foo") 時
handler.defineProperty()
// 在定義代理對象某個屬性時的屬性描述時觸發(fā)該操作竭缝,比如在執(zhí)行 Object.defineProperty(proxy, "foo", {}) 時
handler.has()
// 在判斷代理對象是否擁有某個屬性時觸發(fā)該操作,比如在執(zhí)行 "foo" in proxy 時
handler.get()
// 在讀取代理對象的某個屬性時觸發(fā)該操作沼瘫,比如在執(zhí)行 proxy.foo 時
handler.set()
// 在給代理對象的某個屬性賦值時觸發(fā)該操作抬纸,比如在執(zhí)行 proxy.foo = 1 時
handler.deleteProperty()
// 在刪除代理對象的某個屬性時觸發(fā)該操作,比如在執(zhí)行 delete proxy.foo 時
handler.ownKeys()
// 在獲取代理對象的所有屬性鍵時觸發(fā)該操作耿戚,比如在執(zhí)行 Object.getOwnPropertyNames(proxy) 時
handler.apply()
// 在調(diào)用一個目標(biāo)對象為函數(shù)的代理對象時觸發(fā)該操作湿故,比如在執(zhí)行 proxy() 時阿趁。
handler.construct()
// 在給一個目標(biāo)對象為構(gòu)造函數(shù)的代理對象構(gòu)造實例時觸發(fā)該操作,比如在執(zhí)行 new proxy() 時
適用場景
- 虛擬代理:
- 圖片預(yù)加載(loading 圖)
- 合并HTTP請求(數(shù)據(jù)上報匯總)
- 緩存代理:(前提本體是純函數(shù))
- 緩存異步請求數(shù)據(jù)
- 緩存較復(fù)雜的運算結(jié)果
- ES6 的 Proxy:
- 實現(xiàn)對象私有屬性
- 實現(xiàn)表單驗證
“策略模式” 可應(yīng)用于表單驗證信息坛猪,“代理方式” 也可實現(xiàn)脖阵。這里引用 Github - jawil 的一個例子,思路供大家分享墅茉。
// 利用 proxy 攔截格式不符數(shù)據(jù)
function validator(target, validator, errorMsg) {
return new Proxy(target, {
_validator: validator,
set(target, key, value, proxy) {
let errMsg = errorMsg;
if (value == null || !value.length) {
console.log(`${errMsg[key]} 不能為空`);
return target[key] = false;
}
let va = this._validator[key]; // 這里有策略模式的應(yīng)用
if (!!va(value)) {
return Reflect.set(target, key, value, proxy);
} else {
console.log(`${errMsg[key]} 格式不正確`);
return target[key] = false;
}
}
})
}
// 負(fù)責(zé)校驗的邏輯代碼
const validators = {
name(value) {
return value.length >= 6;
},
passwd(value) {
return value.length >= 6;
},
moblie(value) {
return /^1(3|5|7|8|9)[0-9]{9}$/.test(value);
},
email(value) {
return /^\w+([+-.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/.test(value)
}
}
// 調(diào)用代碼
const errorMsg = {
name: '用戶名',
passwd: '密碼',
moblie: '手機號碼',
email: '郵箱地址'
}
const vali = validator({}, validators, errorMsg)
let registerForm = document.querySelector('#registerForm')
registerForm.addEventListener('submit', function () {
let validatorNext = function* () {
yield vali.name = registerForm.userName.value
yield vali.passwd = registerForm.passWord.value
yield vali.moblie = registerForm.phone.value
yield vali.email = registerForm.email.value
}
let validator = validatorNext();
for (let field of validator) {
validator.next();
}
}
實現(xiàn)思路: 利用 ES6 的 proxy 自定義 handler
的 set()
命黔,進行表單校驗并返回結(jié)果,并且借用 “策略模式" 獨立封裝驗證邏輯就斤。使得表單對象悍募,驗證邏輯,驗證器各自獨立洋机。代碼整潔性坠宴,維護性及復(fù)用性都得到增強。
關(guān)于 “設(shè)計模式” 在表單驗證的應(yīng)用槐秧,可參考 jawil 原文:《探索兩種優(yōu)雅的表單驗證——策略設(shè)計模式和ES6的Proxy代理模式》啄踊。
優(yōu)缺點
- 優(yōu)點:
- 可攔截和監(jiān)聽外部對本體對象的訪問;
- 復(fù)雜運算前可以進行校驗或資源管理刁标;
- 對象職能粒度細分颠通,函數(shù)功能復(fù)雜度降低,符合 “單一職責(zé)原則”膀懈;
- 依托代理顿锰,可額外添加擴展功能,而不修改本體對象启搂,符合 “開發(fā)-封閉原則”
- 缺點:
- 額外代理對象的創(chuàng)建硼控,增加部分內(nèi)存開銷;
- 處理請求速度可能有差別胳赌,非直接訪問存在開銷牢撼,但 “虛擬代理” 及 “緩存代理” 均能提升性能
參考文章
本文首發(fā)Github,期待Star疑苫!
https://github.com/ZengLingYong/blog
作者:以樂之名
本文原創(chuàng)熏版,有不當(dāng)?shù)牡胤綒g迎指出。轉(zhuǎn)載請指明出處捍掺。