代理模式:爲一個對象提供一個代用品或佔位符,以便控制它的訪問。javascript
當咱們不方便直接訪問某個對象時,或不知足需求時,可考慮使用一個替身對象來控制該對象的訪問。替身對象可對請求預先進行處理,再決定是否轉交給本體對象。java
生活小栗子:git
常常 shopping 的同窗,對代購應該不陌生。本身不方便直接購買或買不到某件商品時,會選擇委託給第三方,讓代購或黃牛去作購買動做。程序世界的代理者也是如此,咱們不直接操做原有對象,而是委託代理者去進行。代理者的做用,就是對咱們的請求預先進行處理或轉接給實際對象。github
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');
複製代碼
代理並不會改變本體對象,遵循 「單一職責原則」,即 「自掃門前雪,各找各家」。不一樣對象承擔獨立職責,不過於緊密耦合,具體執行功能仍是本體對象,只是引入代理能夠選擇性地預先處理請求。例如上述代碼中,咱們向 「接聽電話功能」 本體添加了一個屏蔽黑名單的功能(保護代理),預先處理電話接入請求。網絡
虛擬代理的目的,是將開銷大的運算延遲到須要時再執行。app
虛擬代理在圖片預加載的應用,代碼例子來至 《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道判斷題測試,每三道題計分一次,根據計分篩選下一步的三道題目?
三道判斷題得分結果:
總共七種計分結果。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
代理對象的操做,具體的實現方式是在 handler
上定義對象自定義方法集合,以便預先管控對象的操做。
ES6 的 Proxy語法:let proxyObj = new Proxy(target, handler);
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() 時
複製代碼
「策略模式」 可應用於表單驗證信息,「代理方式」 也可實現。這裏引用 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! github.com/ZengLingYon…
做者:以樂之名 本文原創,有不當的地方歡迎指出。轉載請指明出處。