代理模式:爲一個對象提供一個代用品或佔位符,以便控制它的訪問。
當我們不方便直接訪問某個對象時,或不滿足需求時,可考慮使用一個替身對象來控制該對象的訪問。替身對象可對請求預先進行處理,再決定是否轉交給本體對象。
生活小栗子:
- 代購;
- 明星經紀人;
- 和諧上網
經常 shopping 的同學,對代購應該不陌生。自己不方便直接購買或買不到某件商品時,會選擇委託給第三方,讓代購或黃牛去做購買動作。程序世界的代理者也是如此,我們不直接操作原有對象,而是委託代理者去進行。代理者的作用,就是對我們的請求預先進行處理或轉接給實際對象。
模式特點
- 代理對象可預先處理請求,再決定是否轉交給本體;
- 代理和本體對外顯示接口保持一致性
- 代理對象僅對本體做一次包裝
模式細分
- 虛擬代理(將開銷大的運算延遲到需要時執行)
- 緩存代理(爲開銷大的運算結果提供緩存)
- 保護代理(黑白雙簧,代理充當黑臉,攔截非分要求)
- 防火牆代理(控制網絡資源的訪問)
- 遠程代理(爲一個對象在不同的地址控件提供局部代表)
- 智能引用代理(訪問對象執行一些附加操作)
- 寫時複製代理(延遲對象複製過程,對象需要真正修改時才進行)
JavaScript 中常用的代理模式爲 “虛擬代理” 和 “緩存代理”。
模式實現
實現方式:創建一個代理對象,代理對象可預先對請求進行處理,再決定是否轉交給本體,代理和本體對外接口保持一致性(接口名相同)。
// 例子:代理接聽電話,實現攔截黑名單
var backPhoneList = ['189XXXXX140']; // 黑名單列表
// 代理
var ProxyAcceptPhone = function(phone) {
// 預處理
console.log('電話正在接入...');
if (backPhoneList.includes(phone)) {
// 屏蔽
console.log('屏蔽黑名單電話');
} else {
// 轉接
AcceptPhone.call(this, phone);
}
}
// 本體
var AcceptPhone = function(phone) {
console.log('接聽電話:', phone);
};
// 外部調用代理
ProxyAcceptPhone('189XXXXX140');
ProxyAcceptPhone('189XXXXX141');
代理並不會改變本體對象,遵循 “單一職責原則”,即 “自掃門前雪,各找各家”。不同對象承擔獨立職責,不過於緊密耦合,具體執行功能還是本體對象,只是引入代理可以選擇性地預先處理請求。例如上述代碼中,我們向 “接聽電話功能” 本體添加了一個屏蔽黑名單的功能(保護代理),預先處理電話接入請求。
虛擬代理(延遲執行)
虛擬代理的目的,是將開銷大的運算延遲到需要時再執行。
虛擬代理在圖片預加載的應用,代碼例子來至 《JavaScript 設計模式與開發實踐》
// 本體
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); // 圖片加載完設置真實圖片src
}
return {
setSrc: function(src) {
myImage.setSrc('./loading.gif'); // 預先設置圖片src爲loading圖
img.src = src;
}
}
})();
// 外部調用
proxyImage.setSrc('./product.png'); // 有loading圖的圖片預加載效果
緩存代理(暫時存儲)
緩存代理的目的,是爲一些開銷大的運算結果提供暫時存儲,以便下次調用時,參數與結果不變情況下,從緩存返回結果,而不是重新進行本體運算,減少本體調用次數。
應用緩存代理的本體,要求運算函數應是一個純函數,簡單理解比如一個求和函數 sum
, 輸入參數 (1, 1)
, 得到的結果應該永遠是 2
。
純函數:固定的輸入,有固定的輸出,不影響外部數據。
模擬場景:60道判斷題測試,每三道題計分一次,根據計分篩選下一步的三道題目?
三道判斷題得分結果:
- (0, 0 ,0)
- (0, 0, 1)
- (0, 1, 0)
- (0, 1, 1)
- (1, 0, 0)
- (1, 0, 1)
- (1, 1, 0)
- (1, 1, 1)
總共七種計分結果。60/3 = 20
,共進行 20 次計分,每次計分執行 3 個循環累計,共 60 個循環。接下來,借用 “緩存代理” 方式,來實現最少本體運算次數。
// 本體:對三道題答案進行計分
var countScore = function(ansList) {
let [a, b, c] = ansList;
return a + b + c;
}
// 代理:對計分請求預先處理
var proxyCountScore = (function() {
var existScore = {}; // 設定存儲對象
return function(ansList) {
var attr = ansList.join(','); // eg. ['0,0,0']
if (existScore[attr] != null) {
// 從內存返回
return existScore[attr];
} else {
// 內存不存在,轉交本體計算並存入內存
return existScore[attr] = countScore(ansList);
}
}
})();
// 調用計分
proxyCountScore([0,1,0]);
60 道題目,每 3 道題一次計分,共 20 次計分運算,但總的計分結果只有 7 種,那麼實際上本體 countScore()
最多隻需運算 7 次,即可囊括所有計算結果。
通過緩存代理的方式,對計分結果進行臨時存儲。用答案字符串組成屬性名 ['0,1,0']
作爲 key
值檢索內存,若存在直接從內存返回,減少包含複雜運算的本體被調用的次數。之後如果我們的題目增加至 90 道, 120 道,150 道題時,本體 countScore()
運算次數仍舊保持 7 次,中間節省了複雜運算的開銷。
ES6 的 Proxy
ES6新增的 Proxy
代理對象的操作,具體的實現方式是在 handler
上定義對象自定義方法集合,以便預先管控對象的操作。
ES6 的 Proxy語法:let proxyObj = new Proxy(target, handler);
- target: 本體,要代理的對象
- handler: 自定義操作方法集合
- proxyObj: 返回的代理對象,擁有本體的方法,不過會被
handler
預處理
// 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()
// 在讀取代理對象的原型時觸發該操作,比如在執行 Object.getPrototypeOf(proxy) 時
handler.setPrototypeOf()
// 在設置代理對象的原型時觸發該操作,比如在執行 Object.setPrototypeOf(proxy, null) 時
handler.isExtensible()
// 在判斷一個代理對象是否是可擴展時觸發該操作,比如在執行 Object.isExtensible(proxy) 時
handler.preventExtensions()
// 在讓一個代理對象不可擴展時觸發該操作,比如在執行 Object.preventExtensions(proxy) 時
handler.getOwnPropertyDescriptor()
// 在獲取代理對象某個屬性的屬性描述時觸發該操作,比如在執行 Object.getOwnPropertyDescriptor(proxy, "foo") 時
handler.defineProperty()
// 在定義代理對象某個屬性時的屬性描述時觸發該操作,比如在執行 Object.defineProperty(proxy, "foo", {}) 時
handler.has()
// 在判斷代理對象是否擁有某個屬性時觸發該操作,比如在執行 "foo" in proxy 時
handler.get()
// 在讀取代理對象的某個屬性時觸發該操作,比如在執行 proxy.foo 時
handler.set()
// 在給代理對象的某個屬性賦值時觸發該操作,比如在執行 proxy.foo = 1 時
handler.deleteProperty()
// 在刪除代理對象的某個屬性時觸發該操作,比如在執行 delete proxy.foo 時
handler.ownKeys()
// 在獲取代理對象的所有屬性鍵時觸發該操作,比如在執行 Object.getOwnPropertyNames(proxy) 時
handler.apply()
// 在調用一個目標對象爲函數的代理對象時觸發該操作,比如在執行 proxy() 時。
handler.construct()
// 在給一個目標對象爲構造函數的代理對象構造實例時觸發該操作,比如在執行 new proxy() 時
適用場景
-
虛擬代理:
- 圖片預加載(loading 圖)
- 合併HTTP請求(數據上報彙總)
-
緩存代理:(前提本體是純函數)
- 緩存異步請求數據
- 緩存較複雜的運算結果
-
ES6 的 Proxy:
- 實現對象私有屬性
- 實現表單驗證
“策略模式” 可應用於表單驗證信息,“代理方式” 也可實現。這裏引用 Github - jawil 的一個例子,思路供大家分享。
// 利用 proxy 攔截格式不符數據
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]; // 這裏有策略模式的應用
if (!!va(value)) {
return Reflect.set(target, key, value, proxy);
} else {
console.log(`${errMsg[key]} 格式不正確`);
return target[key] = false;
}
}
})
}
// 負責校驗的邏輯代碼
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)
}
}
// 調用代碼
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();
}
}
實現思路: 利用 ES6 的 proxy 自定義 handler
的 set()
,進行表單校驗並返回結果,並且借用 “策略模式" 獨立封裝驗證邏輯。使得表單對象,驗證邏輯,驗證器各自獨立。代碼整潔性,維護性及複用性都得到增強。
關於 “設計模式” 在表單驗證的應用,可參考 jawil 原文:《探索兩種優雅的表單驗證——策略設計模式和ES6的Proxy代理模式》。
優缺點
-
優點:
- 可攔截和監聽外部對本體對象的訪問;
- 複雜運算前可以進行校驗或資源管理;
- 對象職能粒度細分,函數功能複雜度降低,符合 “單一職責原則”;
- 依託代理,可額外添加擴展功能,而不修改本體對象,符合 “開發-封閉原則”
-
缺點:
- 額外代理對象的創建,增加部分內存開銷;
- 處理請求速度可能有差別,非直接訪問存在開銷,但 “虛擬代理” 及 “緩存代理” 均能提升性能
參考文章
Github,期待Star!
https://github.com/ZengLingYong/blog
作者:以樂之名
本文原創,有不當的地方歡迎指出。轉載請指明出處。