代理模式:爲一個對象提供一個代用品或佔位符,以便控制它的訪問。
當咱們不方便直接訪問某個對象時,或不知足需求時,可考慮使用一個替身對象來控制該對象的訪問。替身對象可對請求預先進行處理,再決定是否轉交給本體對象。javascript
生活小栗子:java
常常 shopping 的同窗,對代購應該不陌生。本身不方便直接購買或買不到某件商品時,會選擇委託給第三方,讓代購或黃牛去作購買動做。程序世界的代理者也是如此,咱們不直接操做原有對象,而是委託代理者去進行。代理者的做用,就是對咱們的請求預先進行處理或轉接給實際對象。git
JavaScript 中經常使用的代理模式爲 「虛擬代理」 和 「緩存代理」。github
實現方式:建立一個代理對象,代理對象可預先對請求進行處理,再決定是否轉交給本體,代理和本體對外接口保持一致性(接口名相同)。
// 例子:代理接聽電話,實現攔截黑名單 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圖的圖片預加載效果
緩存代理的目的,是爲一些開銷大的運算結果提供暫時存儲,以便下次調用時,參數與結果不變狀況下,從緩存返回結果,而不是從新進行本體運算,減小本體調用次數。app
應用緩存代理的本體,要求運算函數應是一個純函數,簡單理解好比一個求和函數 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() 時
虛擬代理:
緩存代理:(前提本體是純函數)
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
做者:以樂之名 本文原創,有不當的地方歡迎指出。轉載請指明出處。